前言

本节对以太网电路接口和以太网帧协议做简单的介绍,并在了解了以太网帧协议后,在 FPGA 上实现基于以太网 UDP 帧的发送模块的设计与验证。

提示:以下是本篇文章正文内容,下面案例可供参考

一、ARP 帧的应用场景和存在目的

上一章节,我们介绍了整个以太网传输中最底层的 MAC 帧格式,也提到,MAC 帧并不是直接对用户的数据帧,一段用户信息要想通过以太网传输还需要经过各种传输层协议的层层打包才能最终送入 MAC 帧的数据字段进行传输。无论是传输用户数据还是一些网络通信辅助相关的信息,都需要将数据编码为指定协议后再打包进 MAC 层传输。
所以本节将选择一个最简单的协议 ARP 协议,来介绍以太网 MAC 帧的组包方式,并使用 FPGA 编写 MAC 帧发送器,来发送预先设定好的一段 ARP 协议数据内容,并通过以太网抓包查看数据内容的方式来直观验证 MAC 帧发送器的设计成果。
之所以选择 ARP 协议来辅助进行以太网 MAC 帧发送器的设计,是因为 ARP 协议是所有MAC 帧上层协议中最简单的协议,使用该协议,我们在生成以太网帧的数据和填充字段的内容时会非常的简单方便,而 ARP 协议又能够在 PC 的以太网抓包工具中正确抓取并显示,非常适合我们用来进行最直观的连通性测试。在网络通讯时,源主机的应用程序知道目的主机的 IP 地址和端口号,却不知道目的主机的硬件(MAC)地址,而数据包首先是被网卡接收到再去处理上层协议的,如果接收到的数据包的硬件地址与本机不符,则直接丢弃。因此在通讯前必须获得目的主机的硬件地址。ARP协议就起到这个作用。

二、ARP 帧工作原理

源主机发出 ARP 请求,询问“IP 地址是 192.168.0.1 的主机的硬件地址是多少”,并将这个请求广播到本地网段(以太网帧首部的硬件地址填 FF:FF:FF:FF:FF:FF 表示广播),目的主机接收到广播的 ARP 请求,发现其中的 IP 地址与本机相符,则发送一个 ARP 应答数据包给源主机,将自己的硬件地址填写在应答包中。然后源主机收到该应答报文,就能从报文中解析得知目的主机的硬件地址是多少,然后就能正确的发送数据了。
每台主机都维护一个 ARP 缓存表,可以用 arp -a 命令查看。缓存表中的表项有过期时间(一般为 20 分钟),如果 20 分钟内没有再次使用某个表项,则该表项失效,下次还要发ARP 请求来获得目的主机的硬件地址。由于网络上的 IP 地址本身是动态分配的(Ipv4 资源严重不足,所以采用动态分配机制),上网时每个设备的 IP 地址在不同的时间点其地址都有可能不同,所以某个网络设备在不同的时间,其 IP 地址可能不一样,所以需要用这种缓存表项定期失效的机制来避免 MAC 地址和不同时刻 IP 地址绑定的冲突。下表为完整的 ARP 帧每个字段的数据意义(包含了以太网帧头部字段)。
千兆以太网网络层 ARP 协议的原理与 FPGA 实现-LMLPHP
注意到源 MAC 地址、目的 MAC 地址在以太网首部和 ARP 请求中各出现一次,这两个字段的内容值是不一样的。在发送 ARP 请求帧时,以太网首部中的目的 MAC 地址为广播地址,也就是 6 字节的 0xFF。而 ARP 数据报中,当发送请求报文时,由于目的 MAC 地址暂时无法知晓,所以将其值填充为 0。ARP 返回报文中的值由于已经知晓,所以以太网首部和 ARP数据报中的值用实际值填充。硬件类型指链路层网络类型,1 为以太网,协议类型指要转换的地址类型,0x0800 为 IP
地址。后面两个地址长度对于以太网地址和 IP 地址分别为 6 和 4(字节)。op 字段为 1 表示
ARP 请求,op 字段为 2 表示 ARP 应答。每个字段具体的功能描述如下表所示。
千兆以太网网络层 ARP 协议的原理与 FPGA 实现-LMLPHP
千兆以太网网络层 ARP 协议的原理与 FPGA 实现-LMLPHP

三、以太网 ARP 帧发包实例设计

`timescale 1ns/1ns
module eth_send(
	rst_n,
	
	tx_go,
	data_length,
	
	des_mac,
	src_mac,
	type_length,
	CRC_Result,
	CRC_EN,
	
	//数据fifo
	fifo_rdreq,
	fifo_rddata,
	fifo_rdclk,
	
	//GMII 接口信号	
	gmii_tx_clk,
	gmii_tx_en,
	gmii_tx_er,
	gmii_tx_data
);

	input rst_n; //复位输入
	input tx_go; //发送启动信号,单时钟周期高脉冲使能一次发送
	
	input [47:0]des_mac; //目标MAC地址
	input [47:0]src_mac; //本机/源MAC地址
	input [15:0]type_length;	//数据帧类型
	input [31:0]CRC_Result;	//CRC校验结果
	input [15:0]data_length;	//数据长度
	
	output CRC_EN;
	
	output fifo_rdreq;	//fifo读取请求	
	input [7:0]fifo_rddata;	//fifo读取到数据内容
	output fifo_rdclk;	//fifo读时钟
	
	//MII 接口信号	
	input gmii_tx_clk; //GMII接口发送时钟,由PHY芯片产生,25MHz
	output reg gmii_tx_en;	//GMII接口发送数据使能信号,高电平有效
	output gmii_tx_er;	//发送错误,用以破坏数据包发送
	output reg [7:0]gmii_tx_data;	//GMII接口数据总线,FPGA通过该数据线将需要发送的数据依次送给PHY芯片

	reg [5:0] cnt;		//序列机计数器,本以太网帧发送系统使用线性序列机方式设计
	reg en_tx;	//内部的数据帧发送使能信号,高电平时将数据通过MII接口送出
	wire en_tx_data;	//发送MAC帧中数据部分使能信号,一个完整的MAC帧包含数据和MAC帧头以及结尾的校验部分,本信号用于标识帧中数据段部分
	reg [15:0]data_num;	//待发送的数据帧中数据部分数量
	
	reg [47:0]r_des_mac; //目标MAC地址
	reg [15:0]r_type_length;	//数据帧类型
	
	assign fifo_rdreq = en_tx_data;	//fifo读请求,fifo需要设置为show ahead模式
	
	assign fifo_rdclk = gmii_tx_clk;	//fifo读时钟
	
	always@(posedge gmii_tx_clk)
	if(tx_go)
		r_des_mac <= des_mac;
	else
		r_des_mac <= r_des_mac;
		
	always@(posedge gmii_tx_clk)
	if(tx_go)
		r_type_length <= type_length;
	else
		r_type_length <= r_type_length;
	
	//根据发送启动信号产生内部发送使能信号
	always@(posedge gmii_tx_clk or negedge rst_n)
	if(!rst_n)
		en_tx <= #1  1'd0;
	else if(tx_go)
		en_tx <= #1  1'd1;
	else if(cnt >= 27)//一帧数据发送完成,清零发送使能信号
		en_tx <= #1  1'd0;
	
	//主序列机计数器
	always@(posedge gmii_tx_clk or negedge rst_n)
	if(!rst_n)
		cnt <= #1  6'd0;
	else if(en_tx)begin
		if(!en_tx_data) 
			cnt <= #1  cnt + 6'd1;
		else	//在发送整个帧中的数据部分时,计数器暂停
			cnt <= #1  cnt;
	end
	else
		cnt <= #1  6'd0;		
	
	//帧中数据发送使能信号
	assign en_tx_data = (cnt == 23) && (data_num > 1);
	
	//待发送数据计数器,每发送一个数据段中的数据,本计数器减1.
	always@(posedge gmii_tx_clk or negedge rst_n)
	if(!rst_n)
		data_num <= #1  0;
	else if(tx_go)
		data_num <= #1  data_length;
	else if(en_tx_data)
		data_num <= #1  data_num - 1'b1;
	else
		data_num <= #1  data_num;
		
	assign CRC_EN = ((cnt > 9) && (cnt <= 24));
	
	//序列机部分,根据不同的时刻,切换MII接口数据线上的内容,包含前导码、分隔符、目的地址、源地址、以太网帧类型/长度、数据段数据、结尾CRC校验值
	always@(posedge gmii_tx_clk or negedge rst_n)
	if(!rst_n)
		gmii_tx_data <= #1  8'd0;
	else begin
		case(cnt)
			1, 2, 3, 4, 5, 6, 7:
				gmii_tx_data <= #1  8'h55;	//前导码
				
			8: gmii_tx_data <= #1  8'hd5;	//分隔符
			
			//目的MAC地址
			9: gmii_tx_data <= #1  r_des_mac[47:40];
			10: gmii_tx_data <= #1  r_des_mac[39:32];
			11: gmii_tx_data <= #1  r_des_mac[31:24];
			12: gmii_tx_data <= #1  r_des_mac[23:16];
			13: gmii_tx_data <= #1  r_des_mac[15:8];
			14: gmii_tx_data <= #1  r_des_mac[7:0];

			//源MAC地址
			15: gmii_tx_data <= #1  src_mac[47:40];
			16: gmii_tx_data <= #1  src_mac[39:32];
			17: gmii_tx_data <= #1  src_mac[31:24];
			18: gmii_tx_data <= #1  src_mac[23:16];
			19: gmii_tx_data <= #1  src_mac[15:8];
			20: gmii_tx_data <= #1  src_mac[7:0];
			
			//以太网帧类型/长度
			21: gmii_tx_data <= #1  r_type_length[15:8];
			22: gmii_tx_data <= #1  r_type_length[7:0];
			
			//发送数据
			23: gmii_tx_data <= #1  fifo_rddata;
			
			//发送CRC 校验结果
			24: gmii_tx_data <= #1  CRC_Result[31:24];
			25: gmii_tx_data <= #1  CRC_Result[23:16];
			26: gmii_tx_data <= #1  CRC_Result[15:8];
			27: gmii_tx_data <= #1  CRC_Result[7:0];
	
			28: gmii_tx_data <= #1  8'd0;
			default: gmii_tx_data <= #1  8'd0;
		endcase
	end
	
	//MII数据发送使能信号
	always@(posedge gmii_tx_clk or negedge rst_n)
	if(!rst_n)
		gmii_tx_en <= #1  1'b0;
	else if((cnt >= 
09-15 10:46