新闻中心

EEPW首页 > 嵌入式系统 > 设计应用 > FPGA:Ethernet接口

FPGA:Ethernet接口

作者:时间:2024-01-10来源:EEPW编译收藏

以太网全双工协议易于在中实现。 这里的目标是将连接到10BASE-T连接。

本文引用地址:http://www.eepw.com.cn/article/202401/454644.htm

以太网数据包:发送和接收

10BASE-T 接口 0 - 发送以太网流量的方案

在这里,我们演示了如何将以太网流量直接从FPGA发送到PC。

对于此食谱,您需要:
  1. FPGA 开发板,具有 2 个空闲 IO 和一个 20MHz 时钟。

  2. 一台带有以太网卡并安装了 TCP-IP 堆栈的 PC(如果你能浏览 Internet,你就很好)。

  3. (可选)网络集线器或交换机。

1. 将FPGA板连接到以太网

以下是使用以太网集线器或交换机的典型测试设置视图。



使用集线器或交换机可让电脑在执行此实验时保持与常规网络(如果有)的连接。 但您也可以将FPGA直接连接到PC。
我们在这里使用带有外部 20MHz 振荡器的 Pluto 板。

将FPGA板上的两个IO连接到以太网电缆。

  • 如果电缆的另一端连接到集线器或交换机(如上图所示),请使用以太网电缆的引脚 1 和 2。

  • 如果电缆的另一端直接连接到 PC,请使用引脚 3 和 6。

有关引脚编号,请从这张图片中获取帮助:

请注意,极性通常无关紧要,因为信号是差分的,以太网设备可以从输入信号中检测极性。
此外,即使这在实践中有效,我们也无法通过仅将FPGA连接到电缆(我们需要滤波器和变压器)来满足以太网电气要求。 因此,让我们将其视为“实验室”实验。

2.从PC获取网络信息

在命令行中键入“ipconfig /all”。

写下您的“物理地址”和“IP 地址”。

3. 对FPGA进行编程

编译以下 Verilog HDL 代码。

确保:

  • 更新代码中的数据值(“IP 源”、“IP 目标”和“物理地址”)。

  • 为您的电路板分配正确的引脚(只使用了 3 个引脚!

module TENBASET_TxD(clk20, Ethernet_TDp, Ethernet_TDm);

// a 20MHz clock (this code won't work with a different frequency)
input clk20;

// the two differential 10BASE-T outputs
output Ethernet_TDp, Ethernet_TDm;

// "IP source" - put an unused IP - if unsure, see comment below after the source code
parameter IPsource_1 = 192;
parameter IPsource_2 = 168;
parameter IPsource_3 = 0;
parameter IPsource_4 = 44;

// "IP destination" - put the IP of the PC you want to send to
parameter IPdestination_1 = 192;
parameter IPdestination_2 = 168;
parameter IPdestination_3 = 0;
parameter IPdestination_4 = 2;

// "Physical Address" - put the address of the PC you want to send to
parameter PhysicalAddress_1 = 8'h00;
parameter PhysicalAddress_2 = 8'h07;
parameter PhysicalAddress_3 = 8'h95;
parameter PhysicalAddress_4 = 8'h0B;
parameter PhysicalAddress_5 = 8'hFB;
parameter PhysicalAddress_6 = 8'hAF;

//////////////////////////////////////////////////////////////////////
// sends a packet roughly every second
reg [23:0] counter; always @(posedge clk20) counter<=counter+1;
reg StartSending; always @(posedge clk20) StartSending<=&counter;

//////////////////////////////////////////////////////////////////////
// we send a UDP packet, 18 bytes payload

// calculate the IP checksum, big-endian style
parameter IPchecksum1 = 32'h0000C53F + (IPsource_1<<8)+IPsource_2+(IPsource_3<<8)+IPsource_4+
                                                                (IPdestination_1<<8)+IPdestination_2+(IPdestination_3<<8)+(IPdestination_4);
parameter IPchecksum2 =  ((IPchecksum1&32'h0000FFFF)+(IPchecksum1>>16));
parameter IPchecksum3 = ~((IPchecksum2&32'h0000FFFF)+(IPchecksum2>>16));

reg [6:0] rdaddress;
reg [7:0] pkt_data;

always @(posedge clk20)
case(rdaddress)
// Ethernet preamble
  7'h00: pkt_data <= 8'h55;
  7'h01: pkt_data <= 8'h55;
  7'h02: pkt_data <= 8'h55;
  7'h03: pkt_data <= 8'h55;
  7'h04: pkt_data <= 8'h55;
  7'h05: pkt_data <= 8'h55;
  7'h06: pkt_data <= 8'h55;
  7'h07: pkt_data <= 8'hD5;
// Ethernet header
  7'h08: pkt_data <= PhysicalAddress_1;
  7'h09: pkt_data <= PhysicalAddress_2;
  7'h0A: pkt_data <= PhysicalAddress_3;
  7'h0B: pkt_data <= PhysicalAddress_4;
  7'h0C: pkt_data <= PhysicalAddress_5;
  7'h0D: pkt_data <= PhysicalAddress_6;
  7'h0E: pkt_data <= 8'h00;
  7'h0F: pkt_data <= 8'h12;
  7'h10: pkt_data <= 8'h34;
  7'h11: pkt_data <= 8'h56;
  7'h12: pkt_data <= 8'h78;
  7'h13: pkt_data <= 8'h90;
// IP header
  7'h14: pkt_data <= 8'h08;
  7'h15: pkt_data <= 8'h00;
  7'h16: pkt_data <= 8'h45;
  7'h17: pkt_data <= 8'h00;
  7'h18: pkt_data <= 8'h00;
  7'h19: pkt_data <= 8'h2E;
  7'h1A: pkt_data <= 8'h00;
  7'h1B: pkt_data <= 8'h00;
  7'h1C: pkt_data <= 8'h00;
  7'h1D: pkt_data <= 8'h00;
  7'h1E: pkt_data <= 8'h80;
  7'h1F: pkt_data <= 8'h11;
  7'h20: pkt_data <= IPchecksum3[15:8];
  7'h21: pkt_data <= IPchecksum3[ 7:0];
  7'h22: pkt_data <= IPsource_1;
  7'h23: pkt_data <= IPsource_2;
  7'h24: pkt_data <= IPsource_3;
  7'h25: pkt_data <= IPsource_4;
  7'h26: pkt_data <= IPdestination_1;
  7'h27: pkt_data <= IPdestination_2;
  7'h28: pkt_data <= IPdestination_3;
  7'h29: pkt_data <= IPdestination_4;
// UDP header
  7'h2A: pkt_data <= 8'h04;
  7'h2B: pkt_data <= 8'h00;
  7'h2C: pkt_data <= 8'h04;
  7'h2D: pkt_data <= 8'h00;
  7'h2E: pkt_data <= 8'h00;
  7'h2F: pkt_data <= 8'h1A;
  7'h30: pkt_data <= 8'h00;
  7'h31: pkt_data <= 8'h00;
// payload
  7'h32: pkt_data <= 8'h00; // put here the data that you want to send
  7'h33: pkt_data <= 8'h01; // put here the data that you want to send
  7'h34: pkt_data <= 8'h02; // put here the data that you want to send
  7'h35: pkt_data <= 8'h03; // put here the data that you want to send
  7'h36: pkt_data <= 8'h04; // put here the data that you want to send
  7'h37: pkt_data <= 8'h05; // put here the data that you want to send
  7'h38: pkt_data <= 8'h06; // put here the data that you want to send
  7'h39: pkt_data <= 8'h07; // put here the data that you want to send
  7'h3A: pkt_data <= 8'h08; // put here the data that you want to send
  7'h3B: pkt_data <= 8'h09; // put here the data that you want to send
  7'h3C: pkt_data <= 8'h0A; // put here the data that you want to send
  7'h3D: pkt_data <= 8'h0B; // put here the data that you want to send
  7'h3E: pkt_data <= 8'h0C; // put here the data that you want to send
  7'h3F: pkt_data <= 8'h0D; // put here the data that you want to send
  7'h40: pkt_data <= 8'h0E; // put here the data that you want to send
  7'h41: pkt_data <= 8'h0F; // put here the data that you want to send
  7'h42: pkt_data <= 8'h10; // put here the data that you want to send
  7'h43: pkt_data <= 8'h11; // put here the data that you want to send
  default: pkt_data <= 8'h00;
endcase

//////////////////////////////////////////////////////////////////////
// and finally the 10BASE-T's magic
reg [3:0] ShiftCount;
reg SendingPacket;
always @(posedge clk20) if(StartSending) SendingPacket<=1; else if(ShiftCount==14 && rdaddress==7'h48) SendingPacket<=0;
always @(posedge clk20) ShiftCount <= SendingPacket ? ShiftCount+1 : 15;
wire readram = (ShiftCount==15);
always @(posedge clk20) if(ShiftCount==15) rdaddress <= SendingPacket ? rdaddress+1 : 0;
reg [7:0] ShiftData; always @(posedge clk20) if(ShiftCount[0]) ShiftData <= readram ? pkt_data : {1'b0, ShiftData[7:1]};

// generate the CRC32
reg [31:0] CRC;
reg CRCflush; always @(posedge clk20) if(CRCflush) CRCflush <= SendingPacket; else if(readram) CRCflush <= (rdaddress==7'h44);
reg CRCinit; always @(posedge clk20) if(readram) CRCinit <= (rdaddress==7);
wire CRCinput = CRCflush ? 0 : (ShiftData[0] ^ CRC[31]);
always @(posedge clk20) if(ShiftCount[0]) CRC <= CRCinit ? ~0 : ({CRC[30:0],1'b0} ^ ({32{CRCinput}} & 32'h04C11DB7));

// generate the NLP
reg [17:0] LinkPulseCount; always @(posedge clk20) LinkPulseCount <= SendingPacket ? 0 : LinkPulseCount+1;
reg LinkPulse; always @(posedge clk20) LinkPulse <= &LinkPulseCount[17:1];

// TP_IDL, shift-register and manchester encoder
reg SendingPacketData; always @(posedge clk20) SendingPacketData <= SendingPacket;
reg [2:0] idlecount; always @(posedge clk20) if(SendingPacketData) idlecount<=0; else if(~&idlecount) idlecount<=idlecount+1;
wire dataout = CRCflush ? ~CRC[31] : ShiftData[0];
reg qo; always @(posedge clk20) qo <= SendingPacketData ? ~dataout^ShiftCount[0] : 1;
reg qoe; always @(posedge clk20) qoe <= SendingPacketData | LinkPulse | (idlecount<6);
reg Ethernet_TDp; always @(posedge clk20) Ethernet_TDp <= (qoe ? qo : 1'b0);
reg Ethernet_TDm; always @(posedge clk20) Ethernet_TDm <= (qoe ? ~qo : 1'b0);

endmodule

About the "IP source" that you have to choose in the code above, pick something that is compatible with your network, but still unused.
The example network shown above have IPs starting with "192.168.0" (the PC IP is 192.168.0.2 with a mask of 255.255.255.0). So here IPs like 192.168.0.x can be used. To check if an IP is used or not, "ping" it.

4. 传入数据包

在 PC 上运行此 UDP 接收器软件(包括源代码)。

你会得到这样的东西:

发送有用的流量

上面的代码在每个 UDP 数据包中发送 18 个数据字节。 这 18 个字节可以来自任何地方,因此,例如,您可以修改代码以发送 FPGA 引脚的值。

玩得开心发送UDP数据包!

10BASE-T FPGA 接口 1 - 以太网的工作原理

这是对以太网技术的简要介绍。
如果你是新手,你可以从Charles Spurgeon的以太网网站上获得更多细节。

此页面上的评论同样适用于 10BASE-T 和 100BASE-T(后者快 10 倍)。

IEEE 802.3协议

IEEE 10.100 标准中描述了 802/3BASE-T 接口。
该标准可在 IEEE 802.3 标准协会页面上免费获得。 如果您想要副本,请选择最新标准“IEEE 802.3-2002”。 相关章节包括第3章(MAC帧结构)和第14章(10BASE-T)。

RJ-45 连接器

10/100BASE-T 使用 RJ-45 8 针连接器。

它们看起来像这样:


在 8 个引脚中,只使用了 4 个:
  1. TD+(传输+)

  2. TD-(传输-)

  3. RD+(接收+)



  4. RD-(接收-)



一对引脚用于发送(TD+/TD-),一对用于接收(RD+/RD-)。

注意:此引脚排列适用于从计算机输出的 RJ-45。 集线器或交换机的引脚排列是反转的(TD在引脚3和6上,RD在引脚1和2上)。

差分信号

每对都使用差分电信号(差分信号对外部干扰的免疫力更强)。 此外,每对线都绞合在电缆中,这进一步提高了抗扰度。

数据包编码

要在以太网上发送数据,您不能就这样发送;您必须将其封装到以太网数据包中。 数据包包含一个标头,其中包含数据到达目的地所需的信息。

以太网基于共享介质的理念 - 如果一个站点发送数据包,线路上的每个人都会收到它。 每个以太网卡都有一个唯一的ID(“MAC地址”),因此每个卡都可以自动丢弃发往另一个站点的数据包。 MAC 地址长度为 6 个字节(48 位),足以让地球上的每个以太网卡都有一个唯一的编号!

半双工与全双工

以太网最初是使用真正的共享介质(连接到多个站点的单根同轴电缆)构建的。 传输和接收都使用同一根电缆。 所以当然,你不能同时发送和接收。通信是半双工的。

半双工使用称为“CSMA/CD”(带冲突检测的载波侦听多址)的协议: 在半双工中:
  • 在发射之前,每个电台都必须监听以确保线路是空闲的(“载波检测”)。

  • 尽管如此,两个电台仍有可能同时传输。 因此,必须存在一个复杂的协议来中止(“冲突检测”)并在以后恢复传输。

10/100BASE-T 使用“非屏蔽双绞线”电缆(“UTP”电缆)代替同轴电缆。 UTP 电缆允许全双工,因为它们包含用于传输和接收的单独线对。

全双工通信更好:
  • 您可以获得两倍的带宽。

  • 每个站点都有一个专用的介质,可以随时开始传输,而不会出现并发症(CSMA/CD 不再适用)。

所以现在的问题是,如何让您的 10/100BASE-T 网络在全双工模式下工作?

集线器和交换机

10/100BASE-T是一个“星形拓扑”网络。 它需要使用集中器设备将多台计算机连接在一起。

有两种类型的集中器可用:“集线器”或“交换机”。

  • 集线器是一种简单的电子设备,它在每个链路之间提供电气隔离,但仍然将它们“逻辑”地连接在一起。 这迫使通信是半双工的。 更糟糕的是:在任何给定时间,只有一台计算机可以说话。 因此,网络带宽在所有计算机之间共享。

  • 交换机(或“交换机集线器”)是一种更复杂的电子设备,它在电气和逻辑上隔离每个计算机链路。 它通过在重新传输之前在内部存储每个传输的数据来做到这一点。 因此,每台计算机都可以随时说话:这允许全双工通信。 更好的是:每台计算机都具有完整的链路带宽。 而且由于介质不共享,因此每个站点仅接收发给自己的数据包(隐私性得到改善)。

总之,对于 10BASE-T 网络(将数字乘以 10,表示 100BASE-T):
  • 集线器:每个链路上的半双工,所有计算机之间共享 10Mbps。慢。。。

  • 交换:每个链路上的全双工,每台计算机专用 20Mbps(单向 10Mbps)。快!

开关可能会花费更多,但这是非常值得的!

此项目建议使用全双工链路,因为它不实现 CSMA/CD。 使用半双工链路仍然有效,但代价是潜在的数据包丢失(尤其是在使用集线器共享介质时)。

10BASE-T FPGA 接口 2 - 基于以太网的 IP/UDP

让我们专注于以太网/IP/UDP 数据包。
这些数据包易于生产;但功能强大,它们可以在互联网上传播(并被发送到世界任何地方!

下面是一个示例:

55 55 55 55 55 55 55 D5 00 10 A4 7B EA 80 00 12 34 56 78 90 08 00 45 00 00 2E B3 FE 00 00 80 11 05 40 C0 A8 00 2C C0 A8 00 04 04 00 04 00 00 1A 2D E8 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 B3 31 88 1B

继续阅读,它在下面解码。

协议栈

数据包使用三种不同的协议:以太网、IP 和 UDP(从低级到高级协议)。
每个协议都添加了自己的功能,并嵌入到较低级别的协议中。
  • UDP 部分包含要发送的数据(“有效负载”)。

  • IP 部分允许数据包通过 Internet 路由。

  • 以太网部分允许数据包在以太网上本地发送。

UDP 嵌入到 IP 中,而 IP 本身也嵌入到以太网中。 这是如何一起使用网络协议的一个很好的例子。
上面显示的数据包在这里解码:
  • 以太网前导码/SFD(同步器):55 55 55 55 55 55 D55

  • 以太网目标地址:00 10 A4 7B EA 80

  • 以太网源地址:00 12 34 56 78 90

  • 以太网类型: 08 00 (=IP)

  • IP 标头:45 00 00 2E B3 FE 00 00 80

  • IP 协议:11 (=UDP)

  • IP 校验和: 05 40

  • IP 源 (192.168.0.44): C0 A8 00 2C

  • IP 目标 (192.168.0.4): C0 A8 00 04

  • UPD 源端口 (1024):04 00

  • UPD 目标端口 (1024):04 00

  • UDP 有效载荷长度 (18): 00 1A

  • UPD 校验和: 2D E8

  • UDP 有效负载(18 字节):00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11

  • 以太网校验和: B3 31 88 1B

您可以注意到使用的 IP:从“192.168.0.44”到“192.168.0.4”。 这些是本地IP,这个数据包不会走得太远......

创建自己的数据包

这是一个简单的软件,允许创建自定义以太网/IP/UDP 数据包。
在此处下载。源代码包括在内(目前是 Delphi 6 - 在 Delphi XE2 中要小心,因为一个用户报告了错误生成的数据包......



每次按下“发送”按钮时,软件都会构建一个数据包,显示它并将其发送到串行端口(这将在本项目的第 3 部分后面有用)。

10BASE-T FPGA 接口 3 - 发送数据包

现在我们知道需要传输什么,让我们开始吧。
第 2 部分中的软件将原始以太网数据包数据发送到 PC 的串行端口。
我们的FPGA板只需要从串行端口接收它(慢速...),然后通过10BASE-T线对发送(快!)。

让 PC 计算原始数据包数据使我们能够非常轻松地对所有数据包参数进行实验。 这很好,但最终FPGA应该在独立模式下工作,并自行烹饪/发送数据包。

曼彻斯特编码

10BASE-T 网络以 10Mbps(每秒 10 兆比特)的速度工作。

但 10BASE-T 要求这些位是“曼彻斯特编码”的。这需要将位数增加一倍!
  • 逻辑“0”作为高位发送,后跟低位。

  • 逻辑“1”作为低位发送,后跟高位。

因此,我们的 10Mbps 网络实际上需要 20Mbps 的比特流......

FPGA 时钟

简单性决定了我们使用10MHz时钟,并通过将时钟与输出数据位“xXing”来生成20Mbps曼彻斯特比特流。

那可能行得通。但我们正在把握时间。在FPGA设计中不建议这样做。 在这种特殊情况下,这会降低20Mbps的信号质量,因为每个位转换都会出现毛刺(数据输出永远不会与时钟完全重合)。

因此,让我们使用20MHz时钟。

保持链接处于活动状态

即使 10BASE-T 电缆上没有发送数据包,也必须定期发送脉冲(称为“正常链路脉冲”或“NLP”)。 它用于保持连接“活动”。每 16 毫秒左右需要发送一次脉冲。

NLP也可以在称为“自动协商”的过程中被“快速链接脉冲”(FLP)突发所取代。 FLP 携带有关发送方功能的信息,以便电缆两端的硬件可以协商链路参数,例如速度和半双工/全双工状态。

HDL设计

假设要发送的数据包在FPGA的RAM中可用。
ram512 ram(
  .data(ram_input), .wraddress(wraddress), .clock(clk),
  .q(ram_output), .rdaddress(rdaddress)
);

我们假设我们也知道数据包的长度。我们将读取数据包数据并将其发送到以太网上。
首先,我们需要一个开始信号。
wire StartSending; // pulse indicating when to start sending the packet

reg SendingPacket;
always @(posedge clk) if(StartSending) SendingPacket<=1; else if(DoneSending) SendingPacket<=0;

在 20MHz 时,我们要求每个位有 2 个时钟周期。一个 16 位字节需要 8 个时钟。
reg [3:0] ShiftCount; // count from 0 to 15, as 16 clocks are required per 8-bits bytes
always @(posedge clk) if(SendingPacket) ShiftCount <= ShiftCount+1; else ShiftCount <= 0;

当我们到达字节的最后一位时,我们从RAM中读取一个新字节并将其放入移位寄存器中。 移位寄存器每隔一个时钟就会给我们一个新位。
wire readram = (ShiftCount==15); // time to read a new byte from the RAM?
reg [7:0] ShiftData;
always @(posedge clk) if(ShiftCount[0]) ShiftData <= readram ? ram_output : {1'b0, ShiftData[7:1]};

always @(posedge clk) if(readram) rdaddress <= SendingPacket ? rdaddress+1 : 0;

每个数据包都需要以“TP_IDL”结尾(大约 3 位时间的正脉冲,然后是空闲期)。
reg [2:0] idlecount; // enough bits to count 3 bit-times

always @(posedge clk) if(SendingPacket) idlecount<=0; 

else if(~&idlecount) idlecount<=idlecount+1;


最后,曼彻斯特编码器发送位,然后发送TP_IDL。
reg qo; always @(posedge clk) qo <= SendingPacket ? ~ShiftData[0]^ShiftCount[0] : 1;
reg qoe; always @(posedge clk) qoe <= SendingPacket | (idlecount<6);
reg q1; always @(posedge clk) q1 <= (qoe ? qo : 1'b0);
reg q2; always @(posedge clk) q2 <= (qoe ? ~qo : 1'b0);

此处显示的代码略有简化。 例如,缺少保持链接活动所需的 NLP 脉冲。 完整的代码可以在这里找到。

此代码适用于集线器和交换机。 由于我们只使用了 NLP,因此链路是半双工的,当发生冲突时可能会丢失数据包。
交换机显示的丢弃很少 - 由于我们只在这里传输,除非另一个电台发送广播数据包,否则永远不会发生冲突(可以通过使用 FLP 脉冲将链路带入全双工来修复)。
由于发生冲突的可能性很高,集线器显示大量丢包。

传入数据包

FPGA 发送 UDP 数据包。 但是你如何检测它们呢?

这是一个简单的 UDP 测试软件,可以在 PC 上发送和/或接收 UDP 数据包(使用 PC 的以太网网络适配器)。
  • 软件侦听端口 1024。 如果它在此端口上收到数据包,则会显示它获得了多少字节。

  • 该软件还可以在端口 1024 上发送。只需按下“发送 UDP”按钮即可。



您可以向自己发送数据包 - 只需使用您机器自己的 IP。 但在这种情况下,数据包是在内部路由的,永远没有机会进入网络,所以这不太有用。
要查找 PC 的 IP 或以太网 MAC 地址,您可以在命令行中键入“ipconfig /all”。

请注意,端口值 1024 在软件中是硬编码的(在 GUI 中只能配置目标 IP 和有效负载长度)。 这在实践中不是问题,因为您的计算机不太可能有另一个应用程序已经在侦听此特定端口。

10BASE-T FPGA 接口 4 - 接收数据包

接收器可用于 2 件事:
  • 在网络上创建合法端口。

  • “嗅探”网络(监视数据包) - 只需将接收器并行连接到另一个连接即可。

以下是我在本地网络上嗅探的数据包示例:

55 55 55 55 55 55 55 D5 00 C0 02 37 57 28 00 10 A4 7B EA 80 08 00 45 00 00 3C 02 24 00 00 80 01 B7 47 C0 A8 00 04 C0 A8 00 01 08 00 42 5C 02 00 09 00 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 61 62 63 64 65 66 67 68 69 62 31 C5 4E

我会让你剖析它(ping 从“192.168.0.4”到“192.168.0.1”)。

10BASE-T接收器

10BASE-T 接收器由以下部分组成:
  • 差分接收器电路(简单)。

  • 时钟提取电路(困难部分)。

  • 前导码同步器、解串器和以太网校验和检查器(在FPGA中轻松完成)。

差分输入

我们接收来自 RD+/RD- 的差分(2 线)信号。
信号通常是磁耦合的(使用一个小变压器), 然后使用集成比较器或几个晶体管转换为共模信号(1 线加接地)。

我手上没有变压器,也没有足够快的集成比较器。 因此,我使用了电容耦合方案,然后是晶体管。

我的电路原理图是:


第二个晶体管集电极上的2K电阻高于第一个晶体管(1K)上的值,以帮助在输出端获得50%的占空比信号。 否则,输出端的信号不是方形的......

关于电容耦合:我相信它与电感耦合一样好用,但有一个缺点: 当您将电缆插入接收器时,接收器和驱动器之间的电压电位差可能会向接收器发送电流脉冲 (如果驱动器没有被变压器隔离)。 只是我的想法,我在任何地方都没有找到任何相关信息。有人有更多信息吗?

时钟提取

共模数据仍采用曼彻斯特编码。
曼彻斯特编码方案的工作方式是始终在逻辑位的中间创建转换。 请记住:
  • 逻辑“0”以高电平(50ns)后跟低电平(50ns)(故障沿)的形式发送。

  • 逻辑“1”以低电平(50ns)发送,后跟高电平(50ns)(上升沿)。

这意味着转换也可能出现在 2 位之间(如果同一逻辑位连续发送两次)。

我知道有三种基本方法可以进行时钟提取:
  • 使用更快的时钟对编码信号进行过采样,并使用常规FPGA逻辑(边沿检测器和状态机)提取位。

  • 使用带有改进的相位比较器电路的PLL来重新生成发送器使用的时钟。

  • 使用固定延迟不可重新触发的单稳态,其持续时间为 1.5 编码位(75BASE-T 为 10ns)。 单稳态由中间位转换触发,并忽略位间距转换。

我打算尝试这三种技术。现在,让我们尝试第一个。

对信号进行过采样

这就是“蛮力”方法。 优点是完整的解码是在FPGA中完成的。 不方便的是你需要一个高频时钟。
位提取
我决定使用48MHz的采样频率。
进行曼彻斯特解码需要 3 个步骤。
  1. 对传入的数据进行采样和同步。

  2. 检测边缘并启动计数器。诀窍在于,计数器被制作成忽略任何后续边沿,直到它翻转(仅检测曼彻斯特中位转换)。

  3. 一旦检测到边沿,下一个位在 3 个计数后可用 (在48MHz时,周期约为21ns,因此三次计数为63ns,就在下一个位的中间)。 我们将每个位移入一个 8 位移位寄存器。

reg [2:0] in_data;
always @(posedge clk48) in_data <= {in_data[1:0], manchester_data_in};

reg [1:0] cnt;
always @(posedge clk48) if(|cnt || (in_data[2] ^ in_data[1])) cnt<=cnt+1;

reg [7:0] data;
reg new_bit_avail;
always @(posedge clk48) new_bit_avail <= (cnt==3);
always @(posedge clk48) if(cnt==3) data<={in_data[1],data[7:1]};

位进来了!
前导码/SFD同步
到目前为止,我们只有比特同步(我们知道每个比特何时到来,以及它的值)。 我们知道一个字节每 8 位开始一次,但从哪位开始呢?

为了允许字节同步,以太网帧以以下 8 字节序列开头:
55 55 55 55 55 55 55 D5

在二进制中,0x55 是01010101,而 0xD5 是11010101。 此外,以太网指定首先发送 LSB(例如,0xD5 以 1、0、1、0、1、0、1、1 的形式发送)。 因此,我们收到 1 和 0 的交替模式,一旦我们检测到 2 个连续的 1,我们就知道接下来是真正的数据。

reg end_of_Ethernet_frame;

reg [4:0] sync1;
always @(posedge clk48)
if(end_of_Ethernet_frame)
  sync1 <= 0;
else
if(new_bit_avail)
begin
  if(!(data==8'h55 || data==8'hAA)) // not preamble?
    sync1 <= 0;
  else
  if(~&sync1) // if all bits of this "sync1" counter are one, we decide that enough of the preamble
                  // has been received, so stop counting and wait for "sync2" to detect the SFD
    sync1 <= sync1 + 1; // otherwise keep counting
end

reg [9:0] sync2;
always @(posedge clk48)
if(end_of_Ethernet_frame)
  sync2 <= 0;
else
if(new_bit_avail)
begin
  if(|sync2) // if the SFD has already been detected (Ethernet data is coming in)
    sync2 <= sync2 + 1; // then count the bits coming in
  else
  if(&sync1 && data==8'hD5) // otherwise, let's wait for the SFD (0xD5)
    sync2 <= sync2 + 1;
end

wire new_byte_available = new_bit_avail && (sync2[2:0]==3'h0) && (sync2[9:3]!=0);

终于,以太网数据正在涌入!
帧结束
如果在一段时间内未检测到时钟转换,则以太网帧结束。
reg [2:0] transition_timeout;

always @(posedge clk48)
if(in_data[2]^in_data[1]) // transition detected?
  transition_timeout <= 0;
else
if(~&cnt)
  transition_timeout <= transition_timeout + 1;

always @(posedge clk48) end_of_Ethernet_frame <= &transition_timeout;
结束语
独立应用程序需要检查CRC(位于以太网数据包的末尾)是否存在错误的传输错误。
在这里,我只是将完整的数据发送到 PC - 它显示它或做任何它喜欢的事情。

wire [7:0] q_fifo;
fifo myfifo(.data(data), .wrreq(new_byte_available), .wrclk(clk48),
                .q(q_fifo), .rdreq(rdreq), .rdclk(clk), .rdempty(rdempty));

wire TxD_busy;
wire TxD_start = ~TxD_busy & ~rdempty;
assign rdreq = TxD_start;

async_transmitter async_txd(.clk(clk), .TxD(TxD), .TxD_start(TxD_start), .TxD_busy(TxD_busy), .TxD_data(q_fifo));


关键词: FPGA Ethernet接口

评论


相关推荐

技术专区

关闭