赞
踩
此篇为专栏《紫光同创FPGA开发笔记》的第七篇,同时也是 FPGA 以太网入门的第三篇,记录我的学习 FPGA 的一些开发过程和心得感悟,刚接触 FPGA 的朋友们可以先去此博客 《FPGA零基础入门学习路线》来做最基础的扫盲。
本篇内容基于笔者实际开发过程和正点原子资料撰写,将会详细讲解此 FPGA 实验的全流程,诚挚地欢迎各位读者在评论区或者私信我交流!
UDP 是一种面向无连接的传输层协议,属于 TCP/IP 协议簇的一种。UDP 具有消耗资源少、通信效率高等优点,通常用来传输音频、视频等对实时性要求高的场合。本文我们来学习如何使用 FPGA 开发板来实现 UDP 通信的功能。
本文的工程文件开源地址如下(基于ATK-DFPGL22G,大家 clone 到本地就可以直接跑仿真,如果要上板请根据自己的开发板更改约束即可):
本文的实验任务是上位机通过网口调试助手发送数据给 FPGA,FPGA 通过以太网接口接收数据并将接收到的数据发送给上位机,完成以太网 UDP 数据的环回。
UDP(User Datagram Protocol),即用户数据报协议, 是一种面向无连接的传输层协议。无连接是指在传输数据时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。
由于使用 UDP 协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输(如视频会议等)都会采用 UDP 协议进行传输,这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。
UDP 和 TCP 是传输层中非常重要的两个协议,位于 OSI(Open System Interconnection,开放式系统互联)参考模型中的第四层(传输层),是一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务,位于 IP 协议层(网络层)之上。
OSI 将计算机网络体系结构分为七层:物理层、数据链路层、 网络层、传输层、会话层、表示层和应用层,OSI 参考模型如图 2.1 所示。
以太网 UDP 传输单包数据的格式如图 2.2 所示。
从图中可以看出, 以太网的数据包就是对各层协议的逐层封装来实现数据的传输。 用户数据打包在 UDP 协议中,UDP 协议又是基于 IP 协议之上的,IP 协议又是走 MAC 层发送的,即从包含关系来说:MAC 帧中的数据段为 IP 数据报,IP 报文中的数据段为 UDP 报文,UDP 报文中的数据段为用户希望传输的数据内容。接下来我们逐个来向大家介绍不同层的数据格式。
其中以太网的帧格式在本专栏 “以太网 ARP 测试实验” 这篇博客中已经向大家做了详细的介绍,如果对以太网帧格式不熟悉的话,可以参考本专栏 “以太网 ARP 测试实验”。
IP 协议(互联网分组交换协议)是 TCP/IP 协议簇中非常重要的一个协议,下面我们来熟悉下 IP 协议。
IP 协议是 TCP/IP 协议簇中的核心协议,也是 TCP/IP 协议的载体,IP 协议规定了数据传输时的基本单元和格式。从前面介绍的 图 2.2 中可以看出,IP 协议位于以太网 MAC 帧格式的数据段,IP 协议内容由 IP 首部和数据字段组成。所有的 TCP、UDP 及 ICMP 数据都以 IP 数据报格式传输,IP 数据包格式如图 2.3 所示。
前 20 个字节和紧跟其后的可选字段是 IP 数据报的首部,前 20 个字节是固定的,后面可选字段是可有可无的,首部的每一行以 32 位(4 个字节)为单位。
名称 | 解释 |
---|---|
版本 | 4 位 IP 版本号(Version),这个值设置为二进制的 0100 时表示 IPv4,设置为 0110 时表示 IPv6,目前使用比较多的 IP 协议版本号是 4。 |
首部长度 | 4 位首部长度(IHL,Internet Header Length),表示 IP 首部一共有多少个 32 位(4 个字节)。在没有可选字段时,IP 首部长度为 20 个字节,因此首部长度的值为 5。 |
服务类型 | 8 位服务类型(TOS,Type of service),该字段被划分成两个子字段:3 位优先级字段(现在已经基本忽略掉了)和 4 位 TOS 字段,最后一位固定为 0。服务类型为 0 时表示一般服务。 |
总长度 | 16 位 IP 数据报总长度(Total Length),包括 IP 首部和 IP 数据部分,以字节为单位。我们利用 IP 首部长度和 IP 数据报总长度,就可以知道 IP 数据报中数据内容的起始位置和长度。由于该字段长 16bit,所以 IP 数据报最长可达 65535 字节。尽管理论上可以传输长达 65535 字节的 IP 数据报,但实际上还要考虑网络的最大承载能力等因素。 |
标识字段 | 16 位标识(Identification)字段,用来标识主机发送的每一份数据报。通常每发送一份报文它的值就会加 1。 |
标志字段 | 3 位标志(Flags)字段,第 1 位为保留位;第 2 位表示禁止分片(1 表示不分片 0:允许分片);第 3 位标识更多分片(除了数据报的最后一个分片外,其它分片都为 1)。 |
片偏移 | 13 位片偏移(Fragment Offset),在接收方进行数据报重组时用来标识分片的顺序。 |
生存时间 | 8 位生存时间字段,TTL(Time To Live)域防止丢失的数据包在无休止的传播,一般被设置为 64 或者 128。 |
协议 | 8 位协议(Protocol)类型,表示此数据报所携带上层数据使用的协议类型,ICMP 为 1,TCP为 6,UDP 为 17。 |
首部校验和 | 16 位首部校验和(Header Checksum),该字段只校验数据报的首部,不包含数据部分;校验 IP 数据报头部是否被破坏、篡改和丢失等。 |
源 IP 地址 | 32 位源 IP 地址(Source Address),即发送端的 IP 地址,如 192.168.1.123。 |
目的 IP 地址 | 32 位目的 IP 地址(Destination Address),即接收端的 IP 地址, 如 192.168.1.102。 |
可选字段 | 是数据报中的一个可变长度的可选信息,选项字段以 32bit 为界,不足时插入值为 0 的填充字节,保证 IP 首部始终是 32bit 的整数倍。 |
以上内容是对 IP 首部格式的详细阐述,还需要补充的内容是 IP 首部校验和的计算方法,其计算步骤如下:
- 将 16 位检验和字段置为 0,然后将 IP 首部按照 16 位分成多个单元;
- 对各个单元采用反码加法运算(即高位溢出位会加到低位,通常的补码运算是直接丢掉溢出的高位);
- 此时仍然可能出现进位的情况,将得到的和再次分成高 16 位和低 16 位进行累加;
- 最后将得到的和的反码填入校验和字段。
例如,我们使用 IP 协议发送一个 IP 数据报总长度为 50 个字节(有效数据为 30 个字节)的数据包,发送端 IP 地址为 192.168.1.123,接收端 IP 地址为 192.168.102,则 IP 首部数据如图 2.4 所示。
按照上述提到的 IP 首部校验和的方法计算 IP 首部校验和,即:
0x4500 + 0x0032 + 0x0000 + 0x4000 + 0x4011 + 0x0000(计算时强制置 0) + 0xc0a8 + 0x017b + 0xc0a8 + 0x0166 = 0x24974
0x0002 + 0x4974 = 0x4976
0x0000 + 0x4976 = 0x4976(此种情况并未出现进位)
check_sum = ~0x4976(按位取反) = 0xb689
到此为止 IP 协议内容已经介绍完了,我们从前面介绍的 图 2.2 可以知道,UDP 的首部和数据位于 IP 协议的数据段。既然已经有 IP 协议了,为什么还需要 UDP 协议呢?为什么我们选择的是 UDP 还不是传输更可靠的 TCP 呢?
带着这些疑问我们继续往下看。
首先回答为什么还需要 UDP 协议?
事实上数据是可以直接封装在 IP 协议里而不使用 TCP、UDP 或者其它上层协议的。然而在网络传输中同一 IP 服务器需要提供各种不同的服务,各种不同的服务类型是使用端口号来区分的,例如用于浏览网页服务的 80 端口,用于 FTP(文件传输协议)服务的 21 端口等。TCP和 UDP 都使用两个字节的端口号,理论上可以表示的范围为 0 ~ 65535,足够满足各种不同的服务类型。
然后是为什么不选择传输更可靠的 TCP 协议,而是 UDP 协议呢?TCP 协议与 UDP 协议作为传输层最常用的两种传输协议,这两种协议都是使用 IP 作为网络层协议进行传输。
下面是 TCP 协议与 UDP 协议的区别:
- TCP 协议面向连接,是流传输协议,通过连接发送数据,而 UDP 协议传输不需要连接,是数据报协议;
- TCP 为可靠传输协议,而 UDP 为不可靠传输协议。即 TCP 协议可以保证数据的完整和有序,而 UDP 不能保证;
- UDP 由于不需要连接,故传输速度比 TCP 快,且占用资源比 TCP 少;
- 应用场合:TCP 协议常用在对数据文件完整性较高的一些场景中,如文件传输等。UDP 常用于对通讯速度有较高要求或者传输数据较少时,比如对速度要求较高的视频直播和传输数据较少的 QQ 等。
首先可以肯定的告诉大家,使用 FPGA 实现 TCP 协议是完全没有问题的,但是,FPGA 发展到现在,却鲜有成功商用的 RTL 级的 TCP 协议设计,大部分以太网传输都是基于比较简单的 UDP 协议。
TCP 协议设计之初是根据软件灵活性设计的,如果使用硬件逻辑实现,工程量会十分巨大,而且功能和性能无法得到保证,因此,TCP 协议设计并不适合使用硬件逻辑实现。
UDP 协议是一种不可靠传输,发送方只负责数据发送出去,而不管接收方是否正确的接收。在很多场合,是可以接受这种潜在的不可靠性的,例如视频实时传输显示等。
UDP 数据格式如图 2.5 所示。
UDP 首部共 8 个字节,同 IP 首部一样,也是一行以 32 位(4 个字节)为单位。
名称 | 解释 |
---|---|
源端口号 | 16 位发送端端口号,用于区分不同服务的端口,端口号的范围从 0 到 65535。 |
目的端口号 | 16 位接收端端口号。 |
UDP 长度 | 16 位 UDP 长度,包含 UDP 首部长度 + 数据长度,单位是字节(byte)。 |
UDP 校验和 | 16 位 UDP 校验和。UDP 计算校验和的方法和计算 IP 数据报首部校验和的方法相似,但不同的是 IP 数据报的校验和只检验 IP 数据报的首部,而 UDP 校验和包含三个部分:UDP 伪首部,UDP 首部和 UDP 的数据部分。伪首部的数据是从 IP 数据报头和 UDP 数据报头获取的,包括源 IP 地址、目的 IP 地址、协议类型和 UDP 长度,其目的是让 UDP 两次检查数据是否已经正确到达目的地,只是单纯为了做校验用的。在大多数使用场景中接收端并不检测 UDP 校验和,因此这里不做过多介绍。 |
以太网的帧格式、IP 数据报协议以及 UDP 协议到这里已经全部介绍完了,关于用户数据、UDP、IP、MAC 四个报文的关系如图 2.6 所示。
用户数据打包在 UDP 协议中,UDP 协议又是基于 IP 协议之上的,IP 协议又是走 MAC 层发送的,即从包含关系来说:MAC 帧中的数据段为 IP 数据报,IP 报文中的数据段为 UDP 报文,UDP 报文中的数据段为用户希望传输的数据内容。现在再回过头看 图 2.2 的内容就非常容易理解了。
图 3.1 是根据本章实验任务画出的系统框图。和本专栏的 “以太网 ARP 测试实验” 相比,将 ARP 控制模块替换成了以太网控制模块,并增加了一个同步 FIFO 和 UDP 顶层模块。
本次实验虽然实现的是 UDP 通信,但保留了 ARP 顶层模块,这是由于上位机应用程序只知道接收端的目的 IP 地址和端口号,却不知道接收端的 MAC 地址,因此这里通过 ARP 协议来获取接收端的 MAC 地址,否则需要在发送端手动绑定接收端 MAC 地址,而手动绑定的方法较为繁琐,因此这里保留了 ARP 协议。
本次实验同时实现了 ARP 协议和 UDP 协议,GMII 接收侧的引脚同时连接至 ARP 顶层模块和 UDP 顶层模块,这个两个模块会分别根据 ARP 协议和 UDP 协议解析数据。而 GMII 发送侧引脚只能和 ARP 顶层模块和 UDP 顶层模块的其中一个连接,因此以太网控制模块会根据当前接收到的协议类型,选择切换 GMII 发送侧引脚和 ARP 顶层模块或者 UDP 顶层模块连接。
除此之外,以太网控制模块根据输入的 ARP 接收的类型,控制 ARP 顶层模块返回 ARP 应答信号。以太网单次会接收到大量数据, 因此本次实验需要一个 FIFO 模块用来缓存数据,由于本次实验所使用的 GMII 接收时钟和 GMII 发送时钟实际上为同一个时钟,因此这里使用的是同步 FIFO。
GMII TO RGMII 模块负责将双沿(DDR)数据和单沿(SDR)数据之间的转换;ARP 顶层模块解析 ARP 请求命令,并返回开发板的 MAC 地址;以太网控制模块根据输入的 ARP 接收完成信号类型,控制 ARP 顶层模块返回 ARP 应答信号,并根据当前接收到的协议类型,选择切换 ARP 顶层模块和 UDP 顶层模块的 GMII 发送侧引脚;UDP 顶层模块实现了以太网 UDP 数据包的接收、发送以及 CRC 校验的功能。
同步 FIFO 模块是由 Pango 软件自带的 FIFO IP 核生成的,FIFO 的大小为 2048 个 32bit,为了能够满足单包数据量较大的情况(尽管通常情况下,以太网帧有效数据不超过 1500 个字节),所以 FIFO 的深度最好设置的大一点,这里把深度设置为 2048,宽度为 32 位。
FPGA 顶层模块例化了以下五个模块,GMII TO RGMII 模块(gmii_to_rgmii)、ARP 顶层模块(arp)、UDP 顶层模块(udp)、同步 FIFO 模块(sync_fifo_2048x32b)和以太网控制模块(eth_ctrl),实现了各模块之间的数据交互。
其中 GMII TO RGMII(gmii_to_rgmii)模块和 ARP 顶层模块(arp)在本专栏 “以太网 ARP 测试实验” 中已经向大家作了详细的介绍,如果大家对这部分内容不熟悉的话,可以参考本专栏 “以太网 ARP 测试实验”。
本文我们重点介绍 UDP 顶层模块(udp),UDP 顶层模块实现了整个以太网帧格式与 UDP 协议的功能。
UDP 顶层模块例化了 UDP 接收模块(udp_rx)、UDP 发送模块(udp_tx)和 CRC 校验模块(crc32_d8)。
UDP 接收模块(udp_rx):UDP 接收模块较为简单,因为我们不需要对数据做 IP 首部校验也不需要做 CRC 循环冗余校验,只需要判断目的 MAC 地址与开发板 MAC 地址、目的 IP 地址与开发板 IP 地址是否一致即可。
接收模块的解析顺序是:前导码 + 帧起始界定符 → 以太网帧头 → IP 首部 → UDP 首部 → UDP 数据(有效数据) → 接收结束。
IP 数据报一般以 32bit 为单位,为了和 IP 数据报格式保持一致,所以要把 8 位数据转成 32 位数据,因此接收模块实际上是完成了 8 位数据转 32 位数据的功能。
UDP 发送模块(udp_tx):UDP 发送模块和接收模块比较类似,但是多了 IP 首部校验和和 CRC 循环冗余校验的计算。CRC 的校验并不是在发送模块完成,而是在 CRC 校验模块(crc32_d8) 里完成的。
发送模块的发送顺序是:前导码+帧起始界定符 → 以太网帧头 → IP 首部 → UDP 首部 → UDP 数据(有效数据) → CRC 校验。
输入的有效数据为 32 位数据,GMII 接口为 8 位数据接口,因此发送模块实际上完成的是 32 位数据转 8 位数据的功能。
CRC 校验模块(crc32_d8):CRC 校验模块是对 UDP 发送模块的数据(不包括前导码和帧起始界定符)做校验,把校验结果值拼在以太网帧格式的 FCS 字段,如果 CRC 校验值计算错误或者没有的话,那么电脑网卡会直接丢弃该帧导致收不到数据(有些网卡是可以设置不做校验的)。
CRC32 校验在 FPGA 实现的原理是 LFSR(Linear Feedback Shift Register,线性反馈移位寄存器),其思想是各个寄存器储存着上一次 CRC32 运算的结果,寄存器的输出即为 CRC32 的值。
其中 CRC 校验模块和 ARP 模块例化的校验模块完全相同,下面我们重点介绍 UDP 接收模块和 UDP 发送模块。
UDP 接收模块按照 UDP 的数据格式解析数据,并实现将 8 位用户数据转成 32 位数据的功能。由 UDP 的数据格式可知,解析 UDP 数据很适合使用状态机来实现,图 3.2 为 UDP 接收模块的状态跳转图。
接收模块使用三段式状态机来解析以太网包,从图 3.2 可以比较直观的看到每个状态实现的功能以及跳转到下一个状态的条件。
这里需要注意的一点是,在中间状态如前导码错误、MAC 地址错误以及 IP 地址错误时跳转到 st_rx_end 状态而不是跳转到 st_idle 状态。因为中间状态在解析到数据错误时,单包数据的接收还没有结束,如果此时跳转到 st_idle 状态会误把有效数据当成前导码来解析,所以状态跳转到 st_rx_end。
而 eth_rxdv 信号为 0 时,单包数据才算接收结束,所以 st_rx_end 跳转到 st_idle 的条件是 eth_rxdv = 0,准备接收下一包数据。
因为代码较长,只粘贴了第三段状态机的接收数据状态和接收结束状态源代码,代码如图 3.3 所示。
程序中的 st_rx_data 状态表示接收 UDP 的有效数据,在接收完有效数据后,拉高 rec_pkt_done(单包有效数据接收完成)信号,如图 3.3 中第 10 行代码所示。
图 3.4 为接收过程中在线调试采集的波形图,图中 gmii_rx_dv 和 gmii_rxd 为 GMII 接口的接收有效信号和数据,skip_en 为状态机的跳转信号。每次单包数据接收完成都会产生 rec_pkt_done 信号,rec_en 和 rec_data 为收到的数据有效信号和 32 位数据。
UDP 的仿真代码如下所示:
module tb_udp;
//parameter define
parameter T = 8; //时钟周期为8ns
parameter OP_CYCLE = 100; //操作周期(发送周期间隔)
//开发板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;
//目的IP地址 192168.1.10
parameter DES_IP = {8'd192,8'd168,8'd1,8'd10};
//reg define
reg gmii_clk; //时钟信号
reg sys_rst_n; //复位信号
reg tx_start_en;
reg [31:0] tx_data ;
reg [15:0] tx_byte_num;
reg [47:0] des_mac ;
reg [31:0] des_ip ;
reg [3:0] flow_cnt ;
reg [13:0] delay_cnt ;
wire gmii_rx_clk; //GMII接收时钟
wire gmii_rx_dv ; //GMII接收数据有效信号
wire [7:0] gmii_rxd ; //GMII接收数据
wire gmii_tx_clk; //GMII发送时钟
wire gmii_tx_en ; //GMII发送数据使能信号
wire [7:0] gmii_txd ; //GMII发送数据
wire tx_done ;
wire tx_req ;
//*****************************************************
//** main code
//*****************************************************
assign gmii_rx_clk = gmii_clk ;
assign gmii_tx_clk = gmii_clk ;
assign gmii_rx_dv = gmii_tx_en ;
assign gmii_rxd = gmii_txd ;
//给输入信号初始值
initial begin
gmii_clk = 1'b0;
sys_rst_n = 1'b0; //复位
#(T+1) sys_rst_n = 1'b1; //在第(T+1)ns的时候复位信号信号拉高
end
//125Mhz的时钟,周期则为1/125Mhz=8ns,所以每4ns,电平取反一次
always #(T/2) gmii_clk = ~gmii_clk;
always @(posedge gmii_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
tx_start_en <= 1'b0;
tx_data <= 32'h_00_11_22_33;
tx_byte_num <= 1'b0;
des_mac <= 1'b0;
des_ip <= 1'b0;
delay_cnt <= 1'b0;
flow_cnt <= 1'b0;
end
else begin
case(flow_cnt)
'd0 : flow_cnt <= flow_cnt + 1'b1;
'd1 : begin
tx_start_en <= 1'b1; //拉高开始发送使能信号
tx_byte_num <= 16'd10;//设置发送的字节数
flow_cnt <= flow_cnt + 1'b1;
end
'd2 : begin
tx_start_en <= 1'b0;
flow_cnt <= flow_cnt + 1'b1;
end
'd3 : begin
if(tx_req)
tx_data <= tx_data + 32'h11_11_11_11;
if(tx_done) begin
flow_cnt <= flow_cnt + 1'b1;
tx_data <= 32'h_00_11_22_33;
end
end
'd4 : begin
delay_cnt <= delay_cnt + 1'b1;
if(delay_cnt == OP_CYCLE - 1'b1)
flow_cnt <= flow_cnt + 1'b1;
end
'd5 : begin
tx_start_en <= 1'b1; //拉高开始发送使能信号
tx_byte_num <= 16'd30;//设置发送的字节数
flow_cnt <= flow_cnt + 1'b1;
end
'd6 : begin
tx_start_en <= 1'b0;
flow_cnt <= flow_cnt + 1'b1;
end
'd7 : begin
if(tx_req)
tx_data <= tx_data + 32'h11_11_11_11;
if(tx_done) begin
flow_cnt <= flow_cnt + 1'b1;
tx_data <= 32'h_00_11_22_33;
end
end
default:;
endcase
end
end
//例化UDP模块
udp
#(
.BOARD_MAC (BOARD_MAC), //参数例化
.BOARD_IP (BOARD_IP ),
.DES_MAC (DES_MAC ),
.DES_IP (DES_IP )
)
u_udp(
.rst_n (sys_rst_n ),
.gmii_rx_clk (gmii_rx_clk ),
.gmii_rx_dv (gmii_rx_dv ),
.gmii_rxd (gmii_rxd ),
.gmii_tx_clk (gmii_tx_clk ),
.gmii_tx_en (gmii_tx_en),
.gmii_txd (gmii_txd),
.rec_pkt_done (),
.rec_en (),
.rec_data (),
.rec_byte_num (),
.tx_start_en (tx_start_en),
.tx_data (tx_data ),
.tx_byte_num (tx_byte_num),
.des_mac (des_mac ),
.des_ip (des_ip ),
.tx_done (tx_done ),
.tx_req (tx_req)
);
endmodule
第 10 行代码将目的 ip 地址改写成与开发板的 ip 地址一样,因为仿真的原理是开发板的 UDP 的环回,既将 udp 的发送数据直接发送给 UDP 的接收数据,如果两个 ip 地址不一样 error_en 信号会拉高报错。
UDP 发送模块按照 UDP 的数据格式发送数据,并将 32 位用户数据转成 8 位数据的功能,也就是接收模块的逆过程。同样也非常适合使用状态机来完成发送数据的功能,状态跳转图如图 3.5 所示。
发送模块和接收模块有很多相似之处,同样使用三段式状态机来发送以太网包,从上图可以比较直观的看到每个状态实现的功能以及跳转到下一个状态的条件。
发送模块的代码中定义了数组来存储以太网的帧头、IP 首部以及 UDP 的首部,在复位时初始化数组的值,部分源代码如图 3.6 ~ 图 3.10 所示。
省略部分代码……
以上代码在复位时对数组进行初始化。
在图 3.8 中程序的第 5 行至 22 行代码,为 IP 首部数组进行赋值。
图 3.9 中的代码为发送 UDP 数据段的状态。我们前面讲过以太网帧格式的数据部分最少是 46 个字节,去掉 IP 首部字节和 UDP 首部字节后,有效数据至少为 18 个字节,程序设计中已经考虑到这种情况,当发送的有效数据少于 18 个字节时,会在有效数据后面发送补充位,填充的数据为 0。
图 3.10 中的代码为发送 CRC 校验值状态,发送模块的 CRC 校验是由 crc32_d4 模块完成的,发送模块将输入的 crc 的计算结果每 4 位高低位互换,按位取反发送出去。
图 3.11 为发送过程中在线抓取的波形图,图中 tx_start_en 作为开始发送的启动信号,eth_tx_en 和 eth_tx_data 即为 GMII 接口的发送接口。在开始发送以太网帧头时 crc_en 拉高,开始 CRC 校验的计算,在将要发送有效数据时拉高 tx_req(发送数据请求)信号,tx_data 即为待发送的有效数据,在所有数据发送完成后输出 tx_done(发送完成)信号和 crc_clr(CRC 校验值复位)信号。
以太网控制模块的代码如下所示:
module eth_ctrl(
input clk , //系统时钟
input rst_n , //系统复位信号,低电平有效
//ARP相关端口信号
input arp_rx_done, //ARP接收完成信号
input arp_rx_type, //ARP接收类型 0:请求 1:应答
output reg arp_tx_en, //ARP发送使能信号
output arp_tx_type, //ARP发送类型 0:请求 1:应答
input arp_tx_done, //ARP发送完成信号
input arp_gmii_tx_en, //ARP GMII输出数据有效信号
input [7:0] arp_gmii_txd, //ARP GMII输出数据
//UDP相关端口信号
input udp_tx_start_en,//UDP开始发送信号
input udp_tx_done, //UDP发送完成信号
input udp_gmii_tx_en, //UDP GMII输出数据有效信号
input [7:0] udp_gmii_txd, //UDP GMII输出数据
//GMII发送引脚
output gmii_tx_en, //GMII输出数据有效信号
output [7:0] gmii_txd //UDP GMII输出数据
);
//reg define
reg protocol_sw; //协议切换信号
reg udp_tx_busy; //UDP正在发送数据标志信号
reg arp_rx_flag; //接收到ARP请求信号的标志
//*****************************************************
//** main code
//*****************************************************
assign arp_tx_type = 1'b1; //ARP发送类型固定为ARP应答
assign gmii_tx_en = protocol_sw ? udp_gmii_tx_en : arp_gmii_tx_en;
assign gmii_txd = protocol_sw ? udp_gmii_txd : arp_gmii_txd;
//控制UDP发送忙信号
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
udp_tx_busy <= 1'b0;
else if(udp_tx_start_en)
udp_tx_busy <= 1'b1;
else if(udp_tx_done)
udp_tx_busy <= 1'b0;
end
//控制接收到ARP请求信号的标志
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
arp_rx_flag <= 1'b0;
else if(arp_rx_done && (arp_rx_type == 1'b0))
arp_rx_flag <= 1'b1;
else if(protocol_sw == 1'b0)
arp_rx_flag <= 1'b0;
end
//控制protocol_sw和arp_tx_en信号
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
protocol_sw <= 1'b0;
arp_tx_en <= 1'b0;
end
else begin
arp_tx_en <= 1'b0;
if(udp_tx_start_en)
protocol_sw <= 1'b1;
else if(arp_rx_flag && (udp_tx_busy == 1'b0)) begin
protocol_sw <= 1'b0;
arp_tx_en <= 1'b1;
end
end
end
endmodule
以太网控制模块的代码较简单,如果输入的 arp_rx_done(ARP 接收完成信号)为高电平,且 arp_rx_type 为低电平(ARP 接收类型为请求)时,表示接收到 ARP 请求数据包,此时拉高 arp_rx_flag 信号;当 arp_rx_flag 为高电平,且 udp_tx_busy(当前 UDP 发送模块处于空闲状态)信号为低电平时,此时拉高 arp_tx_en 信号,开始控制 ARP 顶层模块发送 ARP 应答数据包,并拉低 protocol_sw 信号,此时 GMII 发送端口信号和 ARP 顶层模块的发送端口信号相连。
当 protocol_sw 等于 1 时,GMII 发送引脚和 UDP GMII 发送引脚相连,否则和 ARP GMII 发送引脚相连,如程序中第 32 行和第 33 行代码所示。
编译工程并生成比特流 .sbit
文件后,此时将下载器一端连接电脑,另一端与开发板上的 JTAG 下载口连接,将网线一端连接开发板的网口,另一端连接电脑的网口或者路由器,接下来连接电源线,并打开开发板的电源开关。
点击 PDS 工具栏的下载按钮,在弹出的 Fabric Configuration
界面中双击 Boundary Scan
, 我们将生成好的 .sbit
流文件下载到开发板中去。
程序下载完成后,PHY 芯片会和电脑网卡进行通信(自协商),如果程序下载正确并且硬件连接无误的话,我们点击电脑右下角的网络图标,会看到本地连接刚开始显示的是正在识别,一段时间之后显示未识别的网络,打开方式如图 4.1 所示(WIN7、WIN10、WIN11 操作可能存在差异, 但基本相同)。
接下来就可以使用网口调试助手进行通信了,该工具的安装包我已经放在本文的 Github 开源工程中。
打开网口调试助手前, 开发板必须硬件连接正确并且程序下载完成。
网口调试助手打开界面如图 4.2 所示。
打开网口调试助手后,协议类型选择:UDP;本地主机地址选择:本地连接的 IP 地址(本文实验是 192.168.1.102);本地主机端口号:1234;设置完成后点击 打开
按钮。如图 4.3 所示。
远程主机选择:192.168.1.10:1234(开发板的 IP 地址和端口号),在这里本机主端口号和远程主机端口号都为 1234,见 udp_tx 模块, 源代码如下所示:
//16 位源端口号: 1234 16 位目的端口号: 1234
ip_head[5] <= {16'd1234,16'd1234};
网口调试助手打开后,在发送文本框中输入数据 “http://www.openedv.com/forum.php” 并点击发送,如图 4.4 所示。
可以看到网口调试助手中接收到数据 “http://www.openedv.com/forum.php”,接收到的数据与发送的数据一致。
这部分发送和接收的数据读者可以自行修改测试,此处借鉴了正点原子的官方例程。
接下来通过 Wireshark 软件抓取网口的数据包,界面如图 4.5 所示。
双击上图所示的以太网或者先选中以太网,再点击上方红框选中的蓝色按钮,即可开始抓取本地连接的数据包,抓取界面如图 4.6 所示。
从上图可以看到, 已经抓取到其它应用程序使用以太网发送的数据包,但是这些数据包并不是开发板发送的数据包,我们这个时候重新在网口调试助手中点击 发送
按钮,可以看到 Wireshark 软件中抓取的数据,如图 4.7 所示。
上图中第 17 行是上位机发送的 ARP 请求数据包,第 18 行是开发板返回的 ARP 应答数据包,第 19 行是上位机发送的 UDP 数据包,第 20 行是开发板返回的 UDP 数据包。双击开发板返回的数据包,可以看到开发板发送的详细数据,如图 4.8 所示。
由上图可知,源 IP 地址(开发板 IP 地址)为 192.168.1.10,目的 IP 地址(电脑 IP 地址)为 192.168.1.102,源端口号和目的端口号都是 1234。上图中下方红框为开发板发送的 16 进制数据(去掉前导码、SFD 和 CRC 值),可以看到,UDP 的用户数据段对应的 ASIC 码为 “http://www.openedv.com/forum.php”。
至此,本专栏中关于 FPGA 以太网入门的第三篇 —— UDP测试实验已经全部讲解完毕,全文篇幅较长,爆肝2万字,建议收藏后精读,有不理解的地方可以关注作者的微信公众号(沂舟无限进步),和作者深度链接~~
希望以上的内容对您有所帮助,诚挚地欢迎各位读者在评论区或者私信我交流!
微博:沂舟Ryan (@沂舟Ryan 的个人主页 - 微博 )
GitHub:ChinaRyan666
微信公众号:沂舟无限进步(内含精品资料及详细教程)
如果对您有帮助的话请点赞支持下吧!
集中一点,登峰造极。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。