FPGA读写DDR


FPGA和DDR颗粒

前言

今年博客产出不多,由于平时还是挺忙的,中间的一段时间部署博客的环境也出了点问题,所以用另一款笔记软件来做知识点记录,趁着年底有空将笔记转移开源一部分到博客上。这一篇是关于FPGA和DDR的,作为工程上的一个实践的记录。希望能帮到有缘人。
对于FPGA来说,内部的存储资源如BRAM、LutRAM是十分金贵的,对于k7-325t这种在性能、资源、价格三者的制衡下取得了良好折中的主流产品来说,也只有16Mb的BRAM。也就是大概440个36K的block。而在FPGA的主要应用方向,网络帧处理、图像处理、数据采样的时候,很多时候都不可避免的需要用到存储资源,如跨时钟域所需要用到的异步FIFO,调用复杂IP核的开销,需要快速读取的查表操作,这些情况下使用BRAM几乎是唯一的选择。在一个项目的评估阶段,如果最后发现是BRAM或者LUT不够了,那一般就只能使用更多资源的片子。

20250227154358

但是,若是算法可以容忍一些存储-读取延迟,或者只是单纯的需要一个存储池,例如网络帧交换中的暂存队列,是一种典型的只关心存储容量,可以容忍一定的延时的场景。因为帧可以在交换机中停留一定时间,只要保证帧的正确性和出队间隔满足IFS。那么此时就可以使用片外的DDR,来大大提升FPGA的数据吞吐能力,来满足我们的需要。例如更深的队列来提升交换机对突发的处理能力、让每个端口都可以有自己独立的队列、不同优先级的队列等。

对于我的一个有关网络帧处理的项目来说。会在一个PC机上插很多张FPGA板卡,板卡会收到很多网络帧,并使用PCIE通道和PC机进行数据的传输。在8张板卡全部插满,流量很大的时候,电脑的PCIE通道速率无法满足实时上传所有的网络包,此时便可以使用每张板卡上的DDR内存,将所有的帧全部暂存到DDR中,然后等到测试结束流量空闲时,使用PCIE依次读取所有板卡的数据包,并保存到电脑上。

理论知识

有关DDR的前置知识

板级知识

这里讨论的都是FPGA与DDR颗粒的一些基础知识,让我们能更好的用起来。挂一漏万。

20250227154417

我之前画过ZYNQ+DDR的PCB板,其中还是有一些门道的,例如在PCB板的摆放位置需要靠近SOC、做好电源去耦、蛇形走线来保证等长、DQ的差分、50欧阻抗控制、端接电阻、多片Fly-by拓扑。在此就不赘述,可以总结为”这就是高速+大容量的代价”。板级工程师秃头.jpg(笑)

对于使用FPGA编写逻辑层的人来说,可以先形成一个这样的基础感性认知-“1片或多片DDR颗粒通过许多根等长的走线连接到了FPGA的特殊管脚上,这些走线中有地址线和数据线

物理特性

而关于DDR的物理特性和操作方法,例如需要预充电,行列选通、每步都需要一定的延迟等,只需要了解,毕竟我们也不是设计DDR的,这些特性对于我们编写FPGA逻辑来说,最后的体现就是-

  1. 从发起写请求到能够开始写入有挺大的延迟(相比于片内逻辑),在200M时钟下大概十几拍。读亦然
  2. 在读取或写入了一页的大小后,需要等待一段时间(AXI的ready会拉低)
  3. 在写入或者读取的时候地址最好是连续的(完美契合AXI的突发特性)

和FPGA的关系

DDR内存并不好驾驭,从DDR1一直到DDR5,内存控制器一直是现代SOC设计的重要部分,频率和带宽也越来越高。其中有很多门道。而FPGA能够使用自己的内部逻辑资源,来实现内存控制器,目前最高支持到DDR4,网上也有一些开源的Verilog编写的DDR控制器的源码。例如

GitHub - ultraembedded/core_ddr3_controller: A DDR3 memory controller in Verilog for various FPGAs A DDR3 memory controller in Verilog for various FPGAs - ultraembedded/core_ddr3_controller https://github.com/ultraembedded/core_ddr3_controller

不过赛灵思官方有MIG这个IP核,它实现了比较完备的DDR控制器的功能,目前支持了DDR2,DDR3,并且提供了原生接口或者AXI4接口供用户逻辑使用。7系列和UltraScale系列都能使用。用户需要做的就是和MIG进行交互,而无需直接过多的关注DDR本身的机制。

AXI总线协议

为了编写FPGA用户侧逻辑与DDR交互,我们需要使用MIG这个IP核,并且使用AXI接口。下面狠狠的介绍一下AXI4。

AXI4(Advanced eXtensible Interface 4)是由ARM公司提出的一种高性能、高频率的总线协议,广泛应用于现代SoC设计中。它是AMBA4.0规范的一部分,主要用于连接处理器、内存控制器、外设等IP核。在我们FPGA设计中,是一种很常见的用于芯片内部逻辑高速互联的协议。区别于串口,SPI,I2C这些芯片间通信的协议。

AXI4的主要特点

  1. 高性能:支持高频率操作,数据带宽=时钟频率*位宽,时钟频率取决于设计,一般来说速度等级高的FPGA能达到300MHz甚至更高,而位宽的大小取决于系统的扇出能力和时序,最高可以支持1024位。
  2. 多通道架构:包括读地址通道、写地址通道、写数据通道、读数据通道和写响应通道,支持并行操作。这就可以让读写同时进行,也就是双工。
  3. 突发传输:支持突发传输(Burst Transfer),可以一次性传输多个数据,提高数据传输效率。
  4. 支持乱序传输:允许数据传输顺序与地址顺序不一致,也就是说可以先给地址再给数据,或者先给数据再给地址,提高系统灵活性。
  5. 多主多从架构:支持多个主设备和从设备同时操作,适用于复杂的系统设计。

AXI4的传输类型

  1. 读传输:主设备通过读地址通道发送读请求,从设备通过读数据通道返回数据。
  2. 写传输:主设备通过写地址通道发送写请求,通过写数据通道发送数据,从设备通过写响应通道返回写操作的响应。

AXI4的版本

AXI4协议有三个主要版本:

  1. AXI4:用于高性能内存映射接口,支持突发传输。
  2. AXI4-Lite:简化版,用于简单的内存映射接口,不支持突发传输。
  3. AXI4-Stream:用于高速数据流传输,没有地址通道,适用于连续数据传输场景。

上面是AXI的一些基础知识,我想说的是,对于我们实际编写代码逻辑的人来说,主要是搞懂AXI的5个通道的概念和握手机制以及突发传输,这样我们编写代码的时候,才能自己写出完备的状态机,来控制数据的传输,并且知道哪些行为需要避免以导致发生状态机死锁。

AXI的5个通道

AXI4 总线的一大特征是它有 5 个独立的传输通道,这些通道都只支持单向传输。也就是只能从发送方(上游)到接收方(下游)。

作为类比,SPI 总线有 2 条单向传输通道:MISO, MOSI。SPI 输入和输出的数据,大路朝天,各走一条。而作为对比, IIC 协议则只有 SDA 一条双向通道,输入输出数据乃至开头的7bit地址只能在这一条通道上分时双向传输。所以会需要开漏结构和上拉电阻来形成一个线与的结构,让全局只有一个设备在发起操作。

单向传输的通道意味着两端的终端节点是有身份差距的,好比水只能从上游流到下流。在 AXI 总线传输中,通道两端分为 Master 主机与 Slave 从机,主机总是发起读写请求的一方。常见的主机有CPU、DMA,而存储介质控制器(比如 DDR 控制器)则是典型的从机。主机可能通过从机读取或者写入存储介质。而显然从机不能主动向 CPU 写入数据。

通道的读/写定义都是从主机的角度来定义的,这5个通道和简写都得记住,以后描述关系的时候需要常用到,那么五个通道都有谁呢:

  1. 读地址通道(Read Address Channel, AR):主设备向从设备发送读地址和控制信息。
  2. 写地址通道(Write Address Channel, AW):主设备向从设备发送写地址和控制信息。
  3. 写数据通道(Write Data Channel, W):主设备向从设备发送写数据。
  4. 读数据通道(Read Data Channel, R):从设备向主设备发送读数据。
  5. 写响应通道(Write Response Channel, B):从设备向主设备发送写操作的响应。

5 个是不是很奇怪,你看读/写怎么也得是个 2 的倍数,那为什么不是 6 条,为什么没有读回复?其实,读回复借用了读数据通道。至于为什么有独立的写回复,而无读回复通道,我们之后再讲。

读写传输操作中的各通道职能

首先是写传输操作(Write transcation)

这下面的图都是来自ARM的官方手册,如下图所示,主机首先在写地址通道(AW)上告知从机本次传输操作(transcaction,对应后文中的”传输操作”)的特性,包括地址和控制信息。

然后,在写数据通道(W)向从机写入数据,一次传输操作中可能包括多次数据传输(data transfer)。

最后,从机在接收到写数据后,通过写回复通道(B)将本次传输操作的响应告知主机。主机以收到从机的响应信号,作为本次传输操作结束的标志。手册中强调,写回复是针对整个传输操作(transcaction)的,而不是针对每次数据传输(data transfer)。

所有传输操作中,写地址AW、写数据W、写响应B的关系都如上图所示,写响应B必然是在传输操作中最后一个写数据之后触发。

但是,写地址与写数据的关系并不局限于一种情况。一般来说,写数据都发生在写地址操作之后,但也不是绝对的,在有些情况下,可以先写数据,或者在同一周期写入数据与地址,都是允许的。这也是 AXI 通道之间的相关性和依赖性的一个体现。(例外:写通道和读通道就没有鸡毛关系)

接下来看读操作(Read transcation):

读操作只涉及两个通道,首先主机在读地址通道AR上写入本次传输操作(Transcation)待读取数据的地址以及控制信息。

从机在接收到地址后,将该地址上的数据通过读数据通道R传输给主机。

值得注意的是, AR 虽然名字为读地址通道,但实际上仍由主机写入地址,只不过是给出要读取数据的地址。读地址通道,这个名字确实有点歧义,主机读操作地址通道表达得更贴切一些。

当然从机发出读数据一定发生在主机写入读地址后,从机不能未卜先知对吧。(预取:?)

无论是读写操作,AXI 总线支持,或者说基于突发传输(Burst Transaction)。简单来说,主机可以写入起始地址以及突发传输的长度等信息,从机将起始地址开始,依次接收主机传输的写数据,或者读取连续地址上的数据,作为读数据传输给主机。所以上面两张图中,一次传输操作中(Transcation) 中包括了一次地址与控制信息(Address & Control)、多个数据(data transfer)。

在读写数据的两条数据通道中,传输突发传输(Burst Transaction)中的最后一个数据,必须要给出 LAST 信号,来标识这是此次突发传输中的最后一次数据传输。

在共同的定义之外,各个通道有自己的定义。

读&写地址通道:

写入本次传输操作所需的地址和控制信息,读写操作都拥有各自的地址通道。

读数据通道:

读数据通道上包括从机发送给主机的读数据,以及从机对于本次读传输操作的回复,具体的读操作状态回复情况会在之后讨论。总线数据位宽可以是 8,16,64,128,256,512 或者是 1024 比特

写数据通道:

写数据通道用于将主机的写数据传输至从机,位宽和读通道的数据位宽相同。写数据通道有一点读数据通道所不具有的特性是拥有 STROBE 信号,也可以称作KEEP或者MOD信号,用于标识写数据中有效的传输字节。即有些无效的数据,出于减少主机工作量的目的,或者在读写宽度不对称时,被放到写数据通道上和有效数据一起发送。而 STROBE 的信号的作用就是标识出这些无用的数据,告知从机不需要接收无用数据。(Master:我只管打包送整坨数据,麻烦标记一下哪些有效)

写数据通道设计有缓存,可超前于从机响应本次传输操作,尽快发起下一次写传输操作。

写回复通道:

用于从机将写操作响应回复给主机。所有写传输操作都需要以写回复通道上接收写响应作为完成信号。再次强调,写回复是针对一次传输操作(transcation)的,而不是针对每一次写数据(data transfer)。

那么问题来了,为什么只有写回复通道而没有读回复通道呢?

这个问题可以从数据流向看出来,主机在读取数据时,数据在读通道上传输,流向为从机到主机。而读回复由从机向主机报告读操作的情况,信号的数据流向也是从机到主机,所以读回复可以合并在读数据通道中,搭个顺风车。

但写回复通道的数据流向就和写数据相反。写数据是从主机到从机,而写回复为从机报告写操作的完成情况,流向为从机到主机,无法合并到写数据通道中,另一方面,写回复又是不可或缺的,所以就有了一条独立的写回复通道。

AXI通道上信号的详细定义

全局信号

AXI 总线中有两个全局信号:ACLK,全局的时钟信号,所有的传输操作都发生在 ACLK 的上升沿。ARESETn,全局复位信号,低电平有效。在复位问题上,AXI 规定了一些细节,后面说。

注意:AResetn 一般是一个同步复位信号,A 代表 AXI,而不是 Async代表的异步。

地址通道

写地址通道

写地址通道所有的前面都有个AW,所以我前面说要记住简写,而且码字的时候常常中文和英文一起写出来,因为写代码的时候时刻要记得这是啥通道。

写地址通道的信号可以分为 3 部分:经常用到的基础信号、突发传输的控制信号、内存访问相关以及其他的不是很常用的信号。

基础信号即 AWADDR:传输操作的起始地址,

AWVALID和AWREADY:握手信号

突发传输指的是传输一次起始地址后,进行多次递增地址上连续的读写操作。

突发传输有关的操作包括

AWLEN:突发传输的长度,即在一次突发传输中数据传输的个数。

AWSIZE:每次突发传输中的数据传输的位宽。

AWBURST:突发传输的类型。

其他信号包括和内存原子操作有关的 AWLOCK,AWCACHE,AWPROT 以及用于用户自定义的 AWUSER 信号,以后可能会讲。(等我自己先用到再说)

读地址通道AR

读地址通道和写地址通道的信号十分类似,就不再从 specification 中截图以及介绍了。

数据通道

写数据通道W

WDATA 与常见的握手信号不再赘述,WDATA 的可使用位宽可以见上文。WSTRB 信号用于标记传输数据中有效的字节,每个 WSTRB 位对应一个字节的位宽,比如数据位宽为 64 位,那么 WSTRB 信号的位宽就是 1 个字节,共 8 位。如果想让数据全部都有效,那就是8个1,也就是8’b11111111,如果想要前3个字节和后4个字节有效,那么就是8’b11110111,注意是从右往左数的。

WLAST 标识一次突发传输中最后一次数据传输,如果没有正确的 WLAST 的信号,就会造成地址无法正确自增,导致从机无法正确接收写数据,从而造成从机不再拉高 READY 信号的现象。

读数据通道R

读数据通道与写数据通道类似,区别有两点:一,支持 RID 信号。二,因为读回复信息在读数据通道上传递,所以集成了 RRESP 信号,用于返回读状态,值得注意的是读回复信号和读数据一样,发送方(source)为从机(slave)。

回复通道

写响应通道B

与写数据通道不同,写回复通道支持 BID,即支持乱序的写回复,关于乱序的问题,我们稍后再谈。BRESP 回复上一次的写状态。

握手机制

发送和接收方的职能

在握手机制中,通信双方分别扮演发送方(Source)和接收方(Destination),两者的操作(职能)并不相同。

发送方置高 VALID 信号表示发送方已经将数据,地址或者控制信息已经就绪,并保持于总线上。

接收方置高 READY 信号表示接收方已经做好接收的准备。

当双方的 VALID/READY 信号同时为高,在时钟 ACLK 上升沿,完成一次数据传输。所有数据传输完毕后,双方同时置低自己的信号。

所谓的双向流控机制,指的是发送方通过置起 VALID 信号控制发送的时机与速度,接收方也可以通过 READY 信号的置起与否控制接收速度。

发送方拥有传输的主动权,但接收方在不具备接收能力时,接收方也能够置低ready信号停止传输,反压发送方 ( 通知发送方自己目前无法处理,发送方此时需有等待机制)。

三种情况

VALID/READY 信号按照到达的先后顺序可以分为 3 种情况:

1.VALID 信号先到达

发送方 VALID 信号早早就到了,这时还不到 T2 时刻,并带来了新鲜的数据(Data)、地址(Addr)或者控制(Ctrl)信息。

但过了 T2 也没见到接收方的 READY 信号。原来是接收方还忙着,可能上一次的数据还没处理完,ready仍为低,过了 T2 才将ready拉高。那么T3 时刻,ready和valid才同时为高,此时传输完成。

在这种情况下,接收方通过 READY 信号控制了传输速度,反压了发送速度。

协议规定在这种情况下 ,VALID 信号一旦置起就不能置低,直到完成握手(handshake occurs)。至少传输一周期数据。

在设计接收方逻辑时,检测到 VALID 信号置起,如果接收方正忙,需让发送方等待,发送方在完成传输之前都不会置低 VALID 信号,不需要考虑发送方撤销传输的可能。而另一种情况则恰恰相反,接收方可以在传输中拉低自己的READY,表明自己目前没有能力处理,需要上游发送方等待。

协议另外规定:发送方不能通过等待接收方 READY 信号来确定置起 VALID 信号的时机。

也就是说发送方准备发送的时候,拉高 VALID 信号是完全主动与独立的过程。因为接收方 READY 信号按照协议可以依赖发送方 VALID 信号,但如果此时发送方也依赖接收方信号,就会造成死锁的情况,所以协议在这里强调了 VALID 信号的主动性。也就是说VALID可以早早给出。在编写状态机跳转条件的时候千万不要觉得需要先判断接受侧READY为高才拉高发送侧VALID。而是发送侧VALID该拉高就拉高,然后再去看READY信号,当VALID和READY同时拉高的时候,才认为发生了传输,进入下一个状态。

此外,AXI协议还有一句On master and slave interfaces, there must be no combinatorial paths between input and output signals.也就是说READY和VALID之间不能有组合逻辑。实际上,最好连打拍也不要有。

2.READY 信号先到达

READY 信号很自由,可以等待 VALID 信号到来再做响应,但也完全可以在 VALID 信号到来前就置高,表示接收端已经做好准备了。

而且,READY 信号与 VALID 不同,接收方可以置起 READY 之后发现:其实我好像还挺忙,然后置低 READY 信号。只要此时 VALID 信号没有置起,这种操作是完全可以。

3.同时到达

同时到达就很简单,等到下一个时钟上升沿 T2,传输就这么轻松愉快地在一个时钟周期里完成了。

突发传输

AXI4总线是基于突发传输的,并且AXI4突发是只需要给一次地址信号即可,这样就免去了多次地址计算的逻辑。对于只存在给一个地址给一个数这样的传输场景,可以使用AXI-lite。

首先还是解释一下为什么要有突发传输这件事。因为很多时候我们需要操作的不仅仅是一个地址的数据,而是从这个地址开始的很多很多数据,这非常好理解。比如去写数组或者读数组,你的数组元素往往有很多很多,因此对于这种情况你就需要突发传输,如果没有突发传输就只能够给出单个地址然后进行操作,然后再给出地址再进行操作。非常的浪费时间和资源。(地址额外的翻转,额外的地址计算逻辑等等)

此外,由于DDR内存本身的物理特性,导致了其读写延迟相较于片内逻辑资源会大出很多。以读操作为例,从给出读地址,到真正有数据过来,这中间的延迟大概有80ns~200ns。如果我们每一次读取,都要先给出地址,再等待数据,那么中间就会有大量的时间浪费。但是DDR又有另一个特性,就是如果读写地址是连续的,那么从第一个数据后,后续地址的数据几乎没有延迟,直到超出一个Page的大小(参见前文的DDR物理特性)。所以这就导致了DDR很适合使用AXI的突发传输,

典型时序图

突发写时序图

从上图可以看到,在T2时刻,AWVALID和AWREADY同时为高,AWADDR写地址上的地址为A,握手成功。

此后WVALID立刻拉高,表明发送方的数据已经准备好,直到T4时刻,WVALID和WREADY同时为高,此时第一次数据传输成功,向地址A0(其实就是A,其中的0表示没有偏移,后续同理)

写入了一次数据。在T6时刻,第二次传输完成。在T8时刻,第三次传输完成。

在T9时刻,给出最后一个写数据的时候,WLAST拉高。

然后B通道返回BRESP和BVALID,代表这一轮的写transaction结束,通信完成。

突发读时序图

可以看到在T2的时刻AR通道握手成功,成功传输了地址信息。然后Slave就可以根据该地址信息返回相应的读数据,每次RVALID和RREADY握手成功的时候,返回了一个数据,称之为一次传输transfer,握手一次返回一次数,当返回最后一笔数据的时候,相应的RLAST也需要拉高,来表示最后一笔数据已经给出,整个Transaction到此结束。这里就没有读响应的事了,直接由Slave给出的RLAST来标识最后一次传输。

之前已经说过,AXI支持Outstanding,在Master给出一个地址以后,可以紧接着再给出一个地址。即使第一个地址的数据还没有返回,这种情况的波形如下图所示:

控制信号

看完了AXI的突发读和突发写时序图,我们进一步了解了突发传输相关的过程,细心的读者可能发现了,上面的控制信号只给了地址信号,写数据大小,突发长度都没有体现,如果没有这些的话,控制器是怎么控制地址自动偏移的呢?实际上面只是简化版本的,忽略了控制信号细节,以下为大家梳理一下,跟突发相关的控制信号总共是有以下三个信号,分别是LEN、SIZE和BURST信号。

以写突发为例

[7:0] AWLEN 写突发个数,实际传输个数 = awlen + 1,比如想传输8次,这个就得是8’d7

[2:0] AWSIZE每次传输的字节数 3’d0-1字节、3’d1-2字节、3’d2-4字节、3’d3-8字节、3’d4-16字节、3’d5-32字节、3’d6-64字节、3’d7-128字节

[1:0] AWBURST 突发传输的类型,2’d0固定地址,2’d1递增,2’d2回卷。

首先是突发长度,AWLEN信号指定了每一轮突发传输有多少次Data Transfer,,对于AXI4,支持1~256笔transfer,也就是8’d0-8’d255

然后是突发数据大小,AWSIZE信号用于标识每次传输Transfer的字节个数,AWSIZE的值必须大于等于AXI总线的数据宽度,

最后是突发传输的类型,AWBRUST是用来控制控制器的地址递增规律,这个有三种类型,各有各自的适应场景。

包括固定地址(FIXED-2’d0)、递增地址(INCR-2’d1)和回环地址(WRAP-2’d2)。每种模式适用于不同的场景:

  • FIXED:适用于重复访问同一地址的场景,例如寄存器操作。
  • INCR:适用于访问连续地址的场景,例如内存读写。
  • WRAP:适用于缓存行填充或循环访问固定大小内存块的场景。
  • 示例
    • 主设备向从设备的某个寄存器写入多个数据。
    • AWBURST = 2'b00,AWLEN = 3(4次传输),AWSIZE = 3'b000(1字节)。
    • 每次传输的地址不变,写入4个字节到同一地址。
  • 示例
    • 主设备向从设备的内存写入16字节数据。
    • AWBURST = 2'b01,AWLEN = 3(4次传输),AWSIZE = 3'b010(4字节)。
    • 每次传输的地址递增4字节,写入4次,总共写入16字节。
  • 示例
    • 主设备从从设备的内存读取一个缓存行(16字节)。
    • AWBURST = 2'b10,AWLEN = 3(4次传输),AWSIZE = 3'b010(4字节)。
    • 地址递增4字节,但在达到16字节边界后回环到起始地址,读取4次,总共读取16字节。

参考资料

五个通道握手机制

突发传输

时序图和示意图

工程实操

扯了这么多理论,我们还是上点实操来Show the Code吧,毕竟我们FPGAer的设计宗旨还是看到仿真才能安心。下面我会仅仅从如何操作的角度来形成一个MIG IP核的设置和调用,编写自己的模块进行数据的存储和读取,最后进行仿真

IP核设置

DDR 控制器 IP 创建流程

在建立好工程后,按如下步骤进行 DDR 控制器 IP 的创建和配置。

1.搜索查找 DDR 控制器 IP

Xilinx 的 DDR 控制器的名称简写为 MIG(Memory Interface Generator),在 Vivado 左侧窗口点击 IP Catalog,然后在 IP Catalog 窗口直接搜索关键字“mig”,就可以很容易的找到Memory Interface Generator(MIG 7 Series)。如下图所示。

20250227155028

2.MIG IP 的配置

进入IP配置界面后,第一个界面是 Memory Interface Generator 介绍页面,如下图所示。默认的器件家族(FPGA Family)、器件 型号(FPGA Part)、速度等级(Speed Grade)、综合工具(Synthesis Tool)和设计输入语言(Design Entry)都和创建工程是保持一致。

20250227155029

点击 Next 到 MIG Output Options 配置页面中,如下图所示。勾选“Create Design”,

默认名称(Component Name)为“mig_7series_0”,用户可对其进行修改,这里保持默认。

选择控制器数量(Number of Controllers)为 1,勾选 AXI4 Interface,如果不选的话,就是另外一种接口,有一定的学习成本,所以如果之前接触过AXI协议,并且符合使用AXI的条件(Verilog,DDR2/3)

20250227155118

点击 Next 到 Pin Compatible FPGAs 配置页面,如下图所示该界面可用于配置选择和当前所设定的器件型号做引脚兼容的其它 FPGA 型号。

对于可能升级器件型号的应用而言,这个功能是很实用的。例如芯片直接从325t无缝升级到480t,而不需要改动DDR布线,这里保持默认不做配置。

下一页选择DDR类型,没什么好说的,是DDR3 SDRAM

20250227155118

20250227155145

3.控制器参数设置

接下来这一页比较重要

20250227155200

在该配置界面需要设定如下重要的 DDR3 存储器信息。对应的设置位置如下图所示。

  1. DDR3 存储器驱动的时钟周期(Clock Period)设置为 2500ps(即 400MHz),这个时钟是用于 FPGA 输出给到 DDR 存储器时钟管脚的时钟。注意这里根据实际情况是有设置区间范围的,例如器件速度等级,高端型号可以支持更高速率,并非可以设置任意值,这里的区间范围为 1072~3300ps
  2. PHY与内存控制器的速度比例:PHY工作在上一行选择的速度,而内存控制器工作在1/4分频或者1/2分频的速率,我这里上面有400MHz,那么内存控制器就工作在100MHz下。1/2有更低的延迟,但是1/4能达到的最大带宽更高。
  3. DDR3 存储器类型(Memory Type)为 Components,也就是颗粒形式的DDR。而不是内存条什么的 。
  4. DDR3 存储器型号(Memory Part)为MT41J256m16XX-125,这是我的板子上板载 DDR3存储器的实际型号(XX 表示任何字符均可)。此处倒三角点击后有很多备选型号,若实际使用型号不在此列表中,可以点击“Create Custom Part”后设置相关 DDR3 存储器的时序参数。
  5. DDR3 存储器接口电压(Memory Voltage)为 1.5V。这里已经根据上面选择的器件定死了。
  6. DDR3 存储器位宽(Data Width)为 64,这里根据实际开发板板载 DDR3 存储器数据总线位宽进行设置。
  7. DDR 控制器的 bank machines 个数设置,这里参数与 DDR3 物理 bank 个数并非是同一概念,设置上并非一定需要与 DDR3 物理 bank 个数保持一致(当然设置相同数量可以增加 DDR 控制器的效率和性能,但是会占用相对多的资源,时序上要求也相对要高,性能和资源上如何达到一个比较好的平衡,需要根据实际应用场景进行设置,有关详细的设置指导可参考文档 7 Series FPGAs Memory Interface Solutions)。
  8. DDR 控制器调度命令的顺序的配置,当选择 strict 时,严格按照命令先后顺序执行;选择 normal 时,为了得到更高的效率,可能对命令重排序。这里我就选择Normal。
    全部参数设置完成后如下图所示。

4.AXI接口参数设置

点击 Next 进入到如下图所示 AXI Parameter 配置页面。该界面是对 AXI 接口相关参数

20250227155213

进行配置,具体配置如下。
(1)AXI 接口的数据位宽,越大的话,在当前时钟域下所能达到的最大速率就越大,但是会增加资源占用和扇出难度,这里设置为 64。
(2)DDR 控制器的仲裁机制,由于 AXI 接口读写通道是独立的,读写各有自己的地址通道,而储存器控制器只有一个地址总线,同一时刻只能进行读或写,这样就存在读/写优先级的问题,这里设置读优先。
(3)Narrow Burst 支持,设置 0,将其关闭。
(4)AXI 接口的地址位宽,自动根据 DDR3 内存生成的位宽,这里 AXI 地址对应的数据是以 1 字节进行计算的,不要与 DDR3 的地址和存储数据混淆。板载 DDR3 存储器存储空间 2Gbit(2Gbit = 256MByte = 2^28 Byte,所以 AXI 的地址位宽为 28)。
(5)AXI 读/写通道的 ID 宽度。ID 是用来标识读/写响应和读/写数据的一致性,具体后面在讲解 AXI 协议会讲。

5.内存参数设置

20250227155222

(1)输入系统时钟频率设置,这个时钟是提供给 MIG IP 的时钟,这里选择400MHz是由于如果选择200MHz,仿真的时候会报错,但是改成400MHz就不会。

(2)突发读类型和长度(Read Burst Type and Length)设置为顺序读写 Sequential。
(3)输出驱动阻抗控制(Output Drive Impedance Control)选择 R ZQ/7。
(4)片上终端(On Die Termination)设置为 R ZQ/4
(5)片选信号(Controller Chip Select Pin)设置为 Enable,即使用该引脚,实际开发板的DDR3 的 CS 信号有连接到 FPGA 管脚,所以这里需要使用该引脚。如果硬件上 DDR3的CS管脚未连接到 FPGA,那么这里就可以设置为 Disable。
(6)DDR 和 AXI 总线之间的地址映射存储器地址映射选择(Memory Address Mapping Selection)。默认选择后者。先给出BANK,再给出ROW,最后给出Column

6.FPGA相关配置

点击 Next 进入到如下图所示的 FPGA Options 配置页面中,做如下设置。

20250227155232

(1)系统时钟(System Clock):这里的系统 200M 时钟由 FPGA 内部提供,不由管脚输入,选择 No Buffer,
(2)参考时钟(Reference Clock):该时钟需要频率为 200MHz 时钟,由于在前面配置中将系统时钟设置为 200MHz,所以可以选择 Use System Clock,这样就可以将两个输入时钟合并一个共用的 200MH 输入。如果前面的系统时钟设置的不是 200MHz 这里配置选项就没有“Use System Clock”可选,只能由管脚端口输入时钟或者 FPGA 内部产生这个 200MHz 时钟。
(3)系统复位极性(System Reset Polarity):选择 ACTIVE LOW。
(4)存储器控制器的调试信号(Debug Signal for Memory Controller)选择 OFF。
(5)其他保留默认设置。

这一页端接选择50欧就行

20250227155246

这里选择固定管脚,因为需要上板子,所以已经知道了管脚是哪些,并且厂家需要提供相关约束文件

20250227155254

这里需要选择Read XDC/UCF 选择厂家给的DDR的XDC文件,或者如果是自己设计的话,就需要一个一个的选择管脚,然后点击Validate,验证是否符合约束关系。

20250227155305

然后就可以一路next直到完成IP核的生成了。至此,我们IP核的配置就告一段落。

DDR仿真的配置

我们需要在自己的工程里调用这个MIG,但是仅仅例化是不够的,还需要我们自己编写AXI接口的逻辑来控制写入和读取。由于我不想在文中加入太多没有普适性操作的内容,而我又不能直接给出我的工程,因为里面有不属于我的知识产权。等我后续从零弄出个只跟DDR相关的demo工程再分享。

此外,因为加入了DDR之后,仿真就会慢上非常多,仿真跑上2ms,不用DDR的时候需要大概2s,有DDR的时候,2分钟还没跑完。而且时间单位会变成ps,所以在日常的时候,我都是通过一个宏,来控制DDR模块是否打开,并且使用了两份tb文件, 要使用DDR的时候才会使用有DDR的那一份。

20250227155800

20250227155817

代码编写

下面这是一份以太网帧转AXI协议的模块。代码先是把以太网帧的数据,SOP,EOP,经过一个位宽转换FIFO,即跨了时钟域,又完成了从以太网的32bit转到AXI的64bit。同时帧长度存入一个寄存长度的FIFO。在空闲态,通过FIFO的非空来开始发起请求,先是通过读寄存长度的FIFO来获取帧长度,用于计算这一次突发的长度,然后进行AXI写地址握手-开始写入-写入结束-响应写回复。其中还要注意需要处理中途READY拉低的HALT状态。

title
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
`timescale 1ns / 1ps
module eth_to_axi (
// Ethernet clock domain (125MHz)
input wire eth_clk,
input wire eth_rst_n,
input [63:0] sys_syn_time, // 全局系统时间

// AXI clock domain (100MHz)
input wire axi_clk,
input wire axi_rst_n,
input wire init_calib_complete, // RAM初始化完成

// Ethernet input interface (32-bit, 125MHz domain)
input wire eth_sop,
input wire eth_eop,
input wire eth_valid,
input wire [ 1:0] eth_mod,
input wire [31:0] eth_data,
input wire [10:0] eth_ddr_len,

// AXI interface to DDR (write) (64-bit, 100MHz domain)
// 写地址
output reg [30:0] axi_awaddr,
output reg [ 7:0] axi_awlen,
output reg axi_awvalid,
input wire axi_awready,

// 写数据
output reg [63:0] axi_wdata,
output reg [ 7:0] axi_wstrb,
output reg axi_wlast,
output reg axi_wvalid,
input wire axi_wready,
// 写响应
input wire [ 1:0] axi_bresp,
input wire axi_bvalid,
output reg axi_bready,

// AXI interface to DDR (read) (64-bit, 100MHz domain)
output reg [30:0] axi_araddr,
output reg [ 7:0] axi_arlen,
output reg axi_arvalid,
input wire axi_arready,
// 读数据
input wire [63:0] axi_rdata,
input wire axi_rlast,
input wire axi_rvalid,
output reg axi_rready,

// Control signals (assumed to be in AXI clock domain)
input wire start_read,
output reg read_done
);
parameter AXI_BASE_WRITE_ADDR = 31'h4000_0000;
parameter AXI_BASE_READ_ADDR = 31'h4000_0000;

// FIFO signals
wire [35:0] fifo_din;
wire fifo_wr_en;
wire fifo_full;
wire [71:0] fifo_dout;
reg fifo_rd_en;
wire fifo_empty;


// Ethernet domain logic
reg [31:0] eth_data_i_reg;
reg eth_sop_i_reg, eth_eop_i_reg, eth_valid_i_reg;
reg [1:0] eth_mod_i_reg;

assign fifo_din = {eth_sop_i_reg, eth_eop_i_reg, eth_mod_i_reg, eth_data_i_reg};
assign fifo_wr_en = eth_valid_i_reg & ~fifo_full;

wire fifo_sop_H, fifo_sop_L, fifo_eop_H, fifo_eop_L;
wire [1:0] fifo_mod_H, fifo_mod_L;
wire [31:0] fifo_data_H, fifo_data_L;


assign {fifo_sop_H, fifo_eop_H, fifo_mod_H, fifo_data_H, fifo_sop_L, fifo_eop_L, fifo_mod_L, fifo_data_L} = fifo_dout;


always @(posedge eth_clk or negedge eth_rst_n) begin
if (eth_rst_n == 1'b0) begin
eth_data_i_reg <= 32'd0;
eth_sop_i_reg <= 1'b0;
eth_eop_i_reg <= 1'b0;
eth_valid_i_reg <= 1'b0;
eth_mod_i_reg <= 2'd0;
end else begin
eth_data_i_reg <= eth_data;
eth_sop_i_reg <= eth_sop;
eth_eop_i_reg <= eth_eop;
eth_valid_i_reg <= eth_valid;
eth_mod_i_reg <= eth_mod;
end
end



wire [8 : 0] fifo_rd_data_count, fifo_wr_data_count;
// 异步FIFO,32bit到64bit位宽转换
Eth_to_AXI_CDC_FIFO eth_to_axi_fifo_inst (
.rst (~eth_rst_n | ~axi_rst_n), // input wire rst
.wr_clk (eth_clk), // input wire wr_clk
.rd_clk (axi_clk), // input wire rd_clk
.din (fifo_din), // input wire [35 : 0] din
.wr_en (fifo_wr_en), // input wire wr_en
.rd_en (fifo_rd_en), // input wire rd_en
.dout (fifo_dout), // output wire [71 : 0] dout
.full (fifo_full), // output wire full
.almost_full (fifo_almost_full), // output wire almost_full
.empty (fifo_empty), // output wire empty
.almost_empty (fifo_almost_empty), // output wire almost_empty
.rd_data_count(fifo_rd_data_count), // output wire [8 : 0] rd_data_count
.wr_data_count(fifo_wr_data_count), // output wire [9 : 0] wr_data_count
.wr_rst_busy (wr_rst_busy), // output wire wr_rst_busy
.rd_rst_busy (rd_rst_busy) // output wire rd_rst_busy
);


reg len_fifo_rd_en;
wire len_fifo_empty, len_fifo_full;
wire [10 : 0] len_fifo_dout;
wire [9 : 0] len_fifo_rd_count, len_fifo_wr_count;
// 异步fifo,保存帧长数据,在以太网最后4字节的eop时写入fifo,单位为字节
len_header_fifo len_header_fifo_inst (
.rst (~eth_rst_n | ~axi_rst_n), // input wire rst
.wr_clk (eth_clk), // input wire wr_clk
.rd_clk (axi_clk), // input wire rd_clk
.din (eth_ddr_len), // input wire [10 : 0] din
.wr_en (eth_eop), // input wire wr_en
.rd_en (len_fifo_rd_en), // input wire rd_en
.dout (len_fifo_dout), // output wire [10 : 0] dout
.full (len_fifo_full), // output wire full
.empty (len_fifo_empty), // output wire empty
.rd_data_count(len_fifo_rd_count), // output wire [9 : 0] rd_data_count
.wr_data_count(len_fifo_wr_count) // output wire [9 : 0] wr_data_count
);


// AXI domain logic
reg [3:0] cstate, nstate;
reg [31:0] bytes_written;
reg [10:0] axi_transfer_beat; // AXI一拍能传输8字节,这一以太网帧需要的拍数
reg [30:0] write_offset; // 写地址偏移,单位为字节
reg [30:0] read_offset; // 写地址偏移,单位为字节


// 状态机状态
localparam WAIT_RAM_INIT = 4'd0;
localparam IDLE = 4'd1;
localparam READ_LEN_FIFO = 4'd2;
localparam WAIT_LEN_FIFO = 4'd3;
localparam CACULATE_PARA = 4'd4;
localparam READ_DATA_FIFO = 4'd5;
localparam WAIT_DATA_FIFO = 4'd6;
localparam WRITE_TO_AXI = 4'd7;
localparam WRITE_SECOND_LAST = 4'd8;
localparam WRITE_LAST = 4'd9;
localparam WRITE_OK_CHECK = 4'd10;
localparam WRITE_OK = 4'd11;
localparam WRITE_HALT = 4'd12;
localparam READ_DDR_CACULATE = 4'd13;
localparam READ_DDR_AXI = 4'd14;
localparam READ_DDR_DONE = 4'd15;


// 状态机次态跳转
always @(posedge axi_clk or negedge axi_rst_n) begin
if (~axi_rst_n) begin
cstate <= WAIT_RAM_INIT;
end else begin
cstate <= nstate;
end
end


// 状态机次态判断逻辑
always @(*) begin
if (axi_rst_n == 1'b0) begin
nstate = WAIT_RAM_INIT;
end
case (cstate)
WAIT_RAM_INIT: begin
if (init_calib_complete == 1'b1) nstate = IDLE;
else nstate = WAIT_RAM_INIT;
end

IDLE: begin
if (len_fifo_empty == 0) nstate = READ_LEN_FIFO;
// else if (sys_syn_time[63:32] == 32'd0 && sys_syn_time[31:0] == 32'd62_500) nstate = READ_DDR_CACULATE; 进READ态,测试一下
else
nstate = IDLE;
end

// ^ 写DDR
READ_LEN_FIFO: begin
nstate = WAIT_LEN_FIFO;
end

WAIT_LEN_FIFO: begin
nstate = CACULATE_PARA;
end

CACULATE_PARA: begin
nstate = READ_DATA_FIFO;
end


READ_DATA_FIFO: begin
nstate = WAIT_DATA_FIFO;
end

WAIT_DATA_FIFO: begin
nstate = WRITE_TO_AXI;
end

WRITE_TO_AXI: begin
if (axi_wready == 1'b1 && axi_wvalid == 1'b1 && axi_transfer_beat == axi_awlen - 2)
nstate = WRITE_SECOND_LAST;
else if (axi_wready == 1'b1) nstate = WRITE_TO_AXI;
else nstate = WRITE_HALT;
end

WRITE_HALT: begin
if (axi_wready == 1'b1 && axi_wvalid == 1'b1 && axi_transfer_beat < axi_awlen - 2)
nstate = WRITE_TO_AXI;
else nstate = WRITE_HALT;
end

WRITE_SECOND_LAST: begin
nstate = WRITE_LAST;
end

WRITE_LAST: begin
nstate = WRITE_OK_CHECK;
end

WRITE_OK_CHECK: begin
if (axi_bready == 1'b1 && axi_bvalid == 1'b1 && axi_bresp == 2'h0) nstate = WRITE_OK;
else nstate = WRITE_OK_CHECK;
end

WRITE_OK: begin
nstate = IDLE;
end

// ^ 读DDR
READ_DDR_CACULATE: begin
nstate = READ_DDR_AXI;
end

READ_DDR_AXI: begin
if (axi_rlast == 1'b1) nstate = READ_DDR_DONE;
else nstate = READ_DDR_AXI;
end

READ_DDR_DONE: begin

end

default: begin
nstate = WAIT_RAM_INIT;
end

endcase
end


// @状态机写逻辑
always @(posedge axi_clk or negedge axi_rst_n) begin
if (axi_rst_n == 1'b0) begin
write_offset <= 31'd0;
read_offset <= 31'd0;
axi_transfer_beat <= 0;
axi_awlen <= 8'd0;
axi_awaddr <= 0;
axi_awvalid <= 0;
axi_wdata <= 0;
axi_wstrb <= 0;
axi_wvalid <= 0;
axi_wlast <= 1'b0;
axi_bready <= 1'b0;
axi_araddr <= 0;
axi_arvalid <= 1'b0;
axi_arlen <= 0;
axi_rready <= 1'b0;
fifo_rd_en <= 1'b0;
len_fifo_rd_en <= 1'b0;
end else
case (cstate)
READ_LEN_FIFO: begin
len_fifo_rd_en <= 1'b1;
end

WAIT_LEN_FIFO: begin
len_fifo_rd_en <= 1'b0;
end

CACULATE_PARA: begin
len_fifo_rd_en <= 1'b0;
axi_transfer_beat <= 0;
axi_awlen <= (len_fifo_dout >> 3) - 1'd1; // axi协议写地址通道的突发个数,为实际拍数-1,比如1024字节,每次传输8字节,需要128次
axi_awaddr <= AXI_BASE_WRITE_ADDR + write_offset; // axi的此次突发传输的写地址
axi_awvalid <= 1'b1;
end

READ_DATA_FIFO: begin
fifo_rd_en <= 1'b1;
axi_awvalid <= 1'b0;
end

WAIT_DATA_FIFO: begin

end

WRITE_HALT: begin
fifo_rd_en <= 1'b0;
end

WRITE_TO_AXI: begin
fifo_rd_en <= 1'b1;
axi_wdata <= {fifo_data_H, fifo_data_L};
axi_wstrb <= 8'b11111111;
axi_wvalid <= 1'b1;
axi_bready <= 1'b1;
axi_transfer_beat <= axi_transfer_beat + 1'b1;
end

WRITE_SECOND_LAST: begin
fifo_rd_en <= 1'b0;
axi_wdata <= {fifo_data_H, fifo_data_L};
axi_wstrb <= 8'b11111111;
axi_wvalid <= 1'b1;
axi_transfer_beat <= axi_transfer_beat + 1'b1;
end


WRITE_LAST: begin
axi_wlast <= 1'b1;
axi_wdata <= {fifo_data_H, fifo_data_L};
axi_wstrb <= 8'b11111111;
axi_wvalid <= 1'b1;
axi_transfer_beat <= axi_transfer_beat + 1'b1;
end


WRITE_OK_CHECK: begin
axi_wlast <= 1'b0;
axi_wvalid <= 1'b0;
end

WRITE_OK: begin
write_offset <= write_offset + len_fifo_dout; // 更新偏移
end

READ_DDR_CACULATE: begin
axi_arlen <= 255; // axi协议写地址通道的突发个数,为实际拍数-1,比如1024字节,每次传输8字节,需要128次
axi_araddr <= AXI_BASE_READ_ADDR + read_offset; // axi的此次突发传输的写地址
axi_arvalid <= 1'b1;
end

READ_DDR_AXI: begin
axi_arvalid <= 1'b0;
axi_rready <= 1'b1;
end

default: begin

end
endcase

end


always @(posedge axi_clk or negedge axi_rst_n) begin
if (axi_rst_n == 1'b0) bytes_written <= 32'd0;
else if (axi_wready == 1'b1 && axi_wvalid == 1'b1)
bytes_written <= bytes_written + 32'd8; // 每拍写入8个字节
end


endmodule

这是DDR控制器的顶层,在这里面例化了MIG和以太网帧转AXI。

title
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
`timescale 1ns / 1ps
// ^

module DDR3_CTRL (
// DDR3 物理接口
inout [63:0] ddr3_dq,
inout [ 7:0] ddr3_dqs_n,
inout [ 7:0] ddr3_dqs_p,
output [14:0] ddr3_addr,
output [ 2:0] ddr3_ba,
output ddr3_ras_n,
output ddr3_cas_n,
output ddr3_we_n,
output ddr3_reset_n,
output [ 0:0] ddr3_ck_p,
output [ 0:0] ddr3_ck_n,
output [ 0:0] ddr3_cke,
output [ 0:0] ddr3_cs_n,
output [ 7:0] ddr3_dm,
output [ 0:0] ddr3_odt,

// Inputs
input sys_clk_i, // Single-ended system clock
input [63:0] sys_syn_time, // 全局系统时间
output mmcm_locked,
output tg_compare_error,
output init_calib_complete,

// selected in GUI.
input sys_rstn, // 复位,低电平有效
output ui_clk, // 输出给用户侧逻辑的clk
output ui_clk_sync_rst, // 时钟同步复位
input clk_user, // 用户侧逻辑输入的时钟

// 用户侧过来的以太网帧
(*mark_debug = "true"*) input [31:0] eth_data,
(*mark_debug = "true"*) input eth_valid,
(*mark_debug = "true"*) input eth_sop,
(*mark_debug = "true"*) input eth_eop,
(*mark_debug = "true"*) input [ 1:0] eth_mod,
(*mark_debug = "true"*) input [10:0] eth_ddr_len

);
parameter C_S_AXI_ID_WIDTH = 4; // Width of all master and slave ID signals. // >= 1.
parameter C_S_AXI_ADDR_WIDTH = 31;
parameter C_S_AXI_DATA_WIDTH = 64;

// Slave Interface Write Address Ports
(*mark_debug = "true"*)wire [ C_S_AXI_ADDR_WIDTH-1:0] s_axi_awaddr;
(*mark_debug = "true"*)wire [ 7:0] s_axi_awlen;
(*mark_debug = "true"*)wire [ 2:0] s_axi_awsize = 3'd3;
(*mark_debug = "true"*)wire [ 1:0] s_axi_awburst = 2'd1;
(*mark_debug = "true"*)wire s_axi_awvalid;
(*mark_debug = "true"*)wire s_axi_awready;

// Slave Interface Write Data Ports
(*mark_debug = "true"*)wire [ C_S_AXI_DATA_WIDTH-1:0] s_axi_wdata;
(*mark_debug = "true"*)wire [(C_S_AXI_DATA_WIDTH/8)-1:0] s_axi_wstrb;
(*mark_debug = "true"*)wire s_axi_wlast;
(*mark_debug = "true"*)wire s_axi_wvalid;
(*mark_debug = "true"*)wire s_axi_wready;

// Slave Interface Write Response Ports
wire s_axi_bready;
wire [ C_S_AXI_ID_WIDTH-1:0] s_axi_bid;
wire [ 1:0] s_axi_bresp;
wire s_axi_bvalid;

// Slave Interface Read Address Ports
(*mark_debug = "true"*)wire [ C_S_AXI_ADDR_WIDTH-1:0] s_axi_araddr;
(*mark_debug = "true"*)wire [ 7:0] s_axi_arlen;
(*mark_debug = "true"*)wire [ 2:0] s_axi_arsize = 3'd3;
(*mark_debug = "true"*)wire [ 1:0] s_axi_arburst = 2'd1;
(*mark_debug = "true"*)wire s_axi_arvalid;
(*mark_debug = "true"*)wire s_axi_arready;

// Slave Interface Read Data Ports
(*mark_debug = "true"*)wire s_axi_rready;
(*mark_debug = "true"*)wire [ C_S_AXI_DATA_WIDTH-1:0] s_axi_rdata;
(*mark_debug = "true"*)wire [ 1:0] s_axi_rresp;
(*mark_debug = "true"*)wire s_axi_rlast;
(*mark_debug = "true"*)wire s_axi_rvalid;
wire [ C_S_AXI_ID_WIDTH-1:0] s_axi_rid;

wire app_sr_active;
wire app_ref_ack;
wire app_zq_ack;


// 将以太网帧转换为AXI时序
eth_to_axi eth_to_axi_inst (
.eth_clk (clk_user),
.eth_rst_n (sys_rstn),
.axi_clk (ui_clk),
.axi_rst_n (~ui_clk_sync_rst),
.sys_syn_time (sys_syn_time),
.init_calib_complete(init_calib_complete),

// 要存入DDR的以太网帧
.eth_sop (eth_sop),
.eth_eop (eth_eop),
.eth_valid (eth_valid),
.eth_mod (eth_mod),
.eth_data (eth_data),
.eth_ddr_len(eth_ddr_len),

// 写地址
.axi_awaddr (s_axi_awaddr),
.axi_awlen (s_axi_awlen),
.axi_awvalid(s_axi_awvalid),
.axi_awready(s_axi_awready),
// 写数据
.axi_wdata (s_axi_wdata),
.axi_wstrb (s_axi_wstrb),
.axi_wlast (s_axi_wlast),
.axi_wvalid (s_axi_wvalid),
.axi_wready (s_axi_wready),
// 写响应
.axi_bresp (s_axi_bresp),
.axi_bvalid (s_axi_bvalid),
.axi_bready (s_axi_bready),

// 读地址
.axi_araddr (s_axi_araddr),
.axi_arlen (s_axi_arlen),
.axi_arvalid(s_axi_arvalid),
.axi_arready(s_axi_arready),
// 读数据
.axi_rdata (s_axi_rdata),
.axi_rlast (s_axi_rlast),
.axi_rvalid (s_axi_rvalid),
.axi_rready (s_axi_rready),

.start_read(start_read),
.read_done (read_done)
);

// DDR3 IP核
mig_7series_0 u_mig_7series_0 (
// Memory interface ports
.ddr3_addr (ddr3_addr), // output [14:0] ddr3_addr 行列地址,其中行地址线和列地址线是分时复用的,即地址要分两次送出,先送出行地址,再送出列地址。
.ddr3_ba(ddr3_ba), // output [2:0] ddr3_ba bank地址
.ddr3_cas_n(ddr3_cas_n), // output ddr3_cas_n 列地址选通
.ddr3_ck_n(ddr3_ck_n), // output [0:0] ddr3_ck_n 差分时钟p端
.ddr3_ck_p(ddr3_ck_p), // output [0:0] ddr3_ck_p 差分时钟n端
.ddr3_cke(ddr3_cke), // output [0:0] ddr3_cke 时钟有效信号,高电平有效
.ddr3_ras_n(ddr3_ras_n), // output ddr3_ras_n 行地址选通信号,低电平有效
.ddr3_reset_n(ddr3_reset_n), // output ddr3_reset_n 复位信号,低电平有效
.ddr3_we_n(ddr3_we_n), // output ddr3_we_n 读写控制 0-写允许 1-读允许
.ddr3_dq (ddr3_dq), // inout [63:0] ddr3_dq 数据 ——这里就和你设置的MIG的数据位宽有直接关系了
.ddr3_dqs_n (ddr3_dqs_n), // inout [7:0] ddr3_dqs_n 数据选取脉冲 ——这里就和你设置的MIG的数据位宽/8有直接关系了
.ddr3_dqs_p (ddr3_dqs_p), // inout [7:0] ddr3_dqs_p 数据选取脉冲 ——这里就和你设置的MIG的数据位宽/8有直接关系了
.init_calib_complete(init_calib_complete), // output init_calib_complete 初始化完成
.ddr3_cs_n(ddr3_cs_n), // output [0:0] ddr3_cs_n 片选信号,低表示命令有效,否则命令屏蔽
.ddr3_dm(ddr3_dm), // output [7:0] ddr3_dm 数据掩码 一位控制一个字节
.ddr3_odt(ddr3_odt), // output [0:0] ddr3_odt 片上终端使能,高电平有效

// 时钟和复位
.sys_clk_i(sys_clk_i), // System Clock Ports
.sys_rst (sys_rstn), // input sys_rst

// Application interface ports
.ui_clk (ui_clk), // output ui_clk
.ui_clk_sync_rst(ui_clk_sync_rst), // output ui_clk_sync_rst
.mmcm_locked (mmcm_locked), // output mmcm_locked
.aresetn (~ui_clk_sync_rst), // input aresetn

.app_sr_req (1'b0), // input app_sr_req
.app_ref_req (1'b0), // input app_ref_req
.app_zq_req (1'b0), // input app_zq_req
.app_sr_active(app_sr_active), // output app_sr_active
.app_ref_ack (app_ref_ack), // output app_ref_ack
.app_zq_ack (app_zq_ack), // output app_zq_ack

// Slave Interface Write Address Ports
.s_axi_awid(4'b0000), // input [3:0] s_axi_awid 写地址的ID
.s_axi_awaddr(s_axi_awaddr), // input [30:0] s_axi_awaddr 写地址
.s_axi_awlen(s_axi_awlen), // input [7:0] s_axi_awlen 写突发个数,实际个数 = awlen + 1
.s_axi_awsize (s_axi_awsize), // input [2:0] s_axi_awsize 每次传输的字节数 3'd0-1字节、3'd1-2字节、3'd2-4字节、3'd3-8字节、3'd4-16字节、3'd5-32字节、3'd6-64字节、3'd7-128字节
.s_axi_awburst(s_axi_awburst), // input [1:0] s_axi_awburst 突发写的类型,2'b00固定。2'b01递增,2'b10回卷
.s_axi_awlock(1'b0), // input [0:0] s_axi_awlock 总线锁信号,没用到
.s_axi_awcache(4'h2), // input [3:0] s_axi_awcache cache类型,没用到
.s_axi_awprot(3'b000), // input [2:0] s_axi_awprot 保护类型,没用到
.s_axi_awqos(4'h0), // input [3:0] s_axi_awqos 服务质量,没用到
.s_axi_awvalid(s_axi_awvalid), // input s_axi_awvalid 主机有效
.s_axi_awready(s_axi_awready), // output s_axi_awready 从机准备好

// Slave Interface Write Data Ports
.s_axi_wdata(s_axi_wdata), // input [63:0] s_axi_wdata 写数据
.s_axi_wstrb(s_axi_wstrb), // input [3:0] s_axi_wstrb 字节有效信号
.s_axi_wlast(s_axi_wlast), // input s_axi_wlast 指示一次突发传输transaction的最后一轮传输
.s_axi_wvalid(s_axi_wvalid), // input s_axi_wvalid 主机有效
.s_axi_wready(s_axi_wready), // output s_axi_wready 从机准备好

// Slave Interface Write Response Ports
.s_axi_bid(s_axi_bid), // output [3:0] s_axi_bid 写响应地址的ID
.s_axi_bresp (s_axi_bresp), // output [1:0] s_axi_bresp 写响应,2'b00 OK, 2'b01 ERR, 2'b10 Slave ERR 2'b11 DECERR
.s_axi_bvalid(s_axi_bvalid), // output s_axi_bvalid 对方来的数据有效
.s_axi_bready(s_axi_bready), // input s_axi_bready 主机这边准备好接受从机来的响应

// Slave Interface Read Address Ports
.s_axi_arid(4'b0000), // input [3:0] s_axi_arid 读地址的ID
.s_axi_araddr(s_axi_araddr), // input [30:0] s_axi_araddr 读地址
.s_axi_arlen(s_axi_arlen), // input [7:0] s_axi_arlen 读突发个数,实际读个数 = arlen + 1
.s_axi_arsize (s_axi_arsize), // input [2:0] s_axi_arsize 每次传输的字节数1、2、4、8、16、32、64、128
.s_axi_arburst(s_axi_arburst), // input [1:0] s_axi_arburst 突发写的类型,2'b00固定。2'b01递增,2'b10回卷
.s_axi_arlock(1'b0), // input [0:0] s_axi_arlock 总线锁信号,没用到
.s_axi_arcache(4'b0010), // input [3:0] s_axi_arcache cache类型,没用到
.s_axi_arprot(3'h0), // input [2:0] s_axi_arprot 保护类型,没用到
.s_axi_arqos(4'h0), // input [3:0] s_axi_arqos 服务质量,没用到
.s_axi_arvalid(s_axi_arvalid), // input s_axi_arvalid 主机有效
.s_axi_arready(s_axi_arready), // output s_axi_arready 从机准备好

// Slave Interface Read Data Ports
.s_axi_rid(s_axi_rid), // output [3:0] s_axi_rid 读数据的ID
.s_axi_rdata(s_axi_rdata), // output [63:0] s_axi_rdata 读数据
.s_axi_rresp (s_axi_rresp), // output [1:0] s_axi_rresp 读响应,2'b00 OK, 2'b01 ERR, 2'b10 Slave ERR 2'b11 DECERR
.s_axi_rlast (s_axi_rlast), // output s_axi_rlast 指示一次突发传输transaction的最后一轮传输
.s_axi_rvalid(s_axi_rvalid), // output s_axi_rvalid 主机有效
.s_axi_rready(s_axi_rready) // input s_axi_rready 从机准备好,此时对方是主机
);


endmodule

20250227155932

最后的仿真结果,能把网络帧一直流式地存储到DDR中。并在特定的时候再读出来。
注意,我省略了一些控制逻辑,聪明的你一定知道怎么补全😉,再放上来就不礼貌了。

总结

总的来说,算是把之前一直想填的坑给填上了。从理论到实操,希望对需要的小伙伴有一些帮助。
我以后复习AXI的时候也会常看常新的。


文章作者: Allen Hong
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Allen Hong !
  目录