作者:李凡
来源:https://zhuanlan.zhihu.com/p/51600261
何为 RAM
RAM -> Ramdom Access Memory ,随机存取存储器。何为随机存取。举个不准确的例子:和上篇文章中的 FIFO 进行对比。对于 FIFO 来说,只有读写两个操作,只能顺序读写。但对于 RAM 来说,同样的读写操作,用户可以在读写时指定读写的地址,实现对整个存储器的乱序(随机)读写访问。
何为 BRAM
BRAM -> Block RAM,花名:块 RAM。
FPGA 中有两种 RAM 资源,另一种 RAM 资源为 Distributed RAM,Distributed RAM 经过综合工具综合,通过多级 LUT 查找表资源级联实现,那么一个 Distributed RAM 可能(综合工具实际应该不会)是由天南地北,相隔很远的 LUT 级联实现的,故名分布式。分布式 RAM 可以利用丰富灵活的 LUT 资源,可以根据使用情况灵活配置,适合对 RAM 延迟要求不高的场合,毕竟并不是"真正"的 RAM。(其实物理上也是真正的 RAM,但不是专用的)
那么 Block RAM 相比分布式 RAM,就是一位专业选手了,BRAM 是 FPGA 厂商在逻辑资源之外,给 FPGA 加入的专用 RAM 块资源。相比分布式 RAM,RAM 块内部以及与逻辑资源之间经过特意的布局布线,使 BRAM 具有很高的运行速度,确定的低延迟周期,但有限的资源数量。
本土主要说明速度,W*D 并不是资源总数。
在网络通信,数字信号处理中应用中,BRAM 都是最重要的资源之一,实现高速数据的缓存,当前最高端的型号拥有近 200MB 的 BRAM 资源。
Nice to Meet BRAM Memory Generator
在 Vivado 中,使用 BRAM Memory Generator 可视化工具生成 BRAM ip 核。
通过在 Ip catlog 中搜索 BRAM,就可以打开 Generator
块/分布式 RAM 有独立的生成工具。可以从 AXI4 一栏了解到该 IP 对 AXI4 协议的支持情况。支持 AXI4,AXI4-Lite,AXI-Stream 或者不支持。(但在 Vivado 中似乎已经没有不支持 AXI4 的 IP 核)
在界面上方的组件名称中,可以设置 IP 核的模块名,并会告诉你某些名字和保留字冲突,不能使用。这里实名吐槽 ISE,在选择 IP 核之前得先确定名字,之后不能改,而且有些无法使用的模块名,还不会提醒。
世界上没有两个完全相同的 ip 核,但它们很多特性是类似的。比如 BRAM 同样支持两种接口类型:Native 或者 AXI4,和 FIFO 一样,本文使用 Native 接口进行讲解,关于 AXI4 协议,可以通过作者的其他文章了解。(就不放链接了啊)
Native 接口
Native 接口分为三类:
- 时钟,复位信号
- 读写地址与数据
- 写使能与 RAM 使能
时钟自不多说,对于 IP 核来说时钟好比,水之于鱼,游戏业务之于腾讯的营收。复位信号是可选的。
读写地址的宽度可以基于 RAM 的深度,log2(深度),也可以固定为 32 位。
RAM 写使能有效时,可以写入数据。在使用 always enable 模式下,读取在每个时钟上升沿有效,不需要额外的使能信号。不然的话 RAM 在读写时都需要 ena 使能信号有效。
Memory 类型
总体上 Memory 按照类型可以分为 RAM 和 ROM,ROM 预置了数据,在使用中只能被读取,不能写入,ROM 实现的物理结构与 RAM 类似,相当于一个只能读取的 ”RAM”。一般用于存放一些固定的参数,比如 FIR 滤波器的参数等等,在使用过程中只需要读取,不需要也不能修改。
按照端口的数量有单端口以及双端口之分,双端口来自于同时对 RAM 进行读写的需求。一边将等待处理的数据从端口 A 输入 RAM,另一端口 B 读取数据进行处理,可以实现高效的数据流式处理,尤其适用于图像的行缓存处理。双端口 RAM 相较于 FIFO ,有可以映射地址以及多次重复利用数据的优势。在新的数据写入之前,可以多次从一指定位置读取旧数据。
双端口 RAM 又可以分为 Simple/Ture 双端口,这方面似乎有点复杂,将在后续的文章中详细分析。本文将主要介绍单端口 RAM 的使用。
读写冲突避免
对于一个 RAM 的读写时序来说,写入时,写使能有效,写入 din 端口上的数据到 addr 端口地址中。读取时,直接从 dout 端口上获得输出 addr 地址上的数据。那么当写使能有效时,用户同时进行读取操作,读取到新数据还是旧数据。这取决于 RAM 的工作模式。
工作模式分为三类:写优先,读优先以及 no change。
写优先,在读写时钟异步的情况下,推荐使用。该模式会保证写操作优先发生。
读优先,该模式以消耗更多 BRAM 资源的前提下,保证每次读操作读取到的都是先前的数据。输入数据会首先被缓存,与此同时在输出总线上输出先前值,一个周期的延迟之后,再输出输入数据。
写入期间数据不变,该模式很“佛性”,在写入期间数据不变,对读写冲突不闻不问,但优点在于节约 BRAM 功耗。
后文将结合仿真,对三种工作模式再进行详细介绍。
PORT 的其他设置
该选项卡中配置 RAM 的宽度与深度;以及是否设置 RAM 使能端,是否设置复位端以及复位后的初试值。
另外重要的一点是配置 RAM 的输出寄存器。共有两个选项 Primitive / Core Output Register。两个选项可以各自选择,都是为输出端添加一级寄存器,不同在于前者在 Port 内部添加寄存器,而后者是在 Port 外部添加寄存器。自然而然,每一级的寄存器都会增加一个周期的读延迟,从初始的 1 周期读延迟,最高可以增加到 3 周期读延迟。
添加寄存器虽然会增加延迟周期数,因为输出信号经过了打拍,但可以减少时钟到数据时间(即从时钟上升沿开始,直到数据出现在输出端口上的时间),改善时序。至于两者之间的区别,本文暂不做讨论。(作者目前也没什么了解)
在 其他选项 中可以为 RAM 指定初始值,类似于 ROM。
我们的数据宽度为 16,深度也为 16 ,但还是使用了一个最小的 18K BRAM实现,着实有些浪费。但 BRAM 使用时的特点就是这样,最小使用单位 9K/18K (取决于器件)。如果想要完全用尽一个 BRAM 块,可以将宽度设为 18,深度设为 1024 。或者较小的 RAM 使用分布式 RAM 实现。
开始仿真
首先,我们观察 RAM 的读延迟。(之前的描述有误,感谢指出的读者 @yq)
将使能端 ena 置高,通过置高写使能信号 wea 一个周期,在时钟上升沿完成一次写入操作。可以观察到,地址总线写入 0x1,在时钟上升沿到来之前准备好了使能信号和地址信号。时钟沿上升沿到来后,完成一次写入操作。
将使能信号和地址在时钟上升沿到来之前准备好
在完成了对地址 0x01 的写入后,就可以通过读取地址 0x01 来观察 BRAM 的读延迟了。
这张仿真图中,读取地址为 0x01,可以观察到在时钟上升沿到来后,经过了 100ps 的延迟之后,0x01 地址上的数据 0xbbbb 出现在 dout 上。
这 100ps 的延迟意味,如果用户逻辑在将地址准备好后之后的第一个上升沿读取数据,那么读取到的数据就是错误的,必须等待下一个时钟上升沿才能够进行读取。从下图手册中的时序说明也可以看到这个延迟。
Latch 代表是 primitive 的输出,reg1 代表的是经过一级输出寄存器后的输出
ena 使能端为低时,会失能 RAM 的读写操作。
ena 信号置低之前,写操作完成,但读操作被失能。
单/双端口 RAM 的应用
在单端口 RAM 中,通常的应用可能是,先进行一系列写入,再进行一系列读取,循环反复。但也可以对一个地址,写入,读取,地址递增,再循环反复。这样的操作类似 FIFO,意义不大。
单端口 BRAM 的应用主要用于缓存,比如需要缓存一些数据,首先将所有数据放入 RAM 中,之后根据需要从不同的地址中取出数据进行运算或者处理。单端口适合读取和写入分时进行的应用。在这类应用中,不会交错进行读取写入。
连续的写入,读取操作可以使用双端口的 RAM 实现,双端口 RAM 有各种独立的地址通道和数据通道,可选各自独立的时钟。
双端口 RAM 的应用很广泛,这里举一个图像处理中的例子。
在图像处理中,图像卷积是一项基本的操作。卷积操作中,需要在将新数据缓存到 RAM 中的同时,从 RAM 中取出旧数据进行卷积运算。这时候读写逻辑控制是分开,双端口提供两套读写控制接口,适合这类读写逻辑相独立的场合。但需要注意的是读写冲突问题,在图像卷积操作中,通过将写地址固定为读地址- 0x2,解决冲突问题。
这里推荐一篇有关 FPGA 图像处理的文章。
https://zhuanlan.zhihu.com/p/38946857
输出寄存
在增加了一级输出寄存器后,如 primitive 输出寄存器后,dout 的数据会出现在地址就绪后的第二个时钟上升沿之后,100 ps 的延迟之后。笔者目前对输出寄存器的作用也还不甚了解。
工作模式
这里通过一个简单的例子,比较了 no change ,read first 以及 write first工作模式。
首先是 no change 模式,在写入期间,输出保持不变,所谓 no change。输出端口上原本为地址 0 的数据:0xaaaa,当地址 1 和地址 2 开始写入时,输出端口保持不变,在写入结束时刻,本来经过 1 /2个周期的延迟,应该输出地址 1 /2上的值:0x1111/0x2222,但是都没有,输出端口保持写入发生之前的地址 0 的数据:0xaaaa。
凡是写入期间,输出保持不变。
只在写入结束后,经过 1 个周期,输出地址 3 上的数据:0x3333 。可见,该模式的特点就是在写入时,保持输出端口不变,但会在读写冲突期间,无法读到地址 1 和地址 2 上的正确数据,读到的都是错误的数据 0xaaaa,地址 0 上的数据。
接下来是 write first 写优先模式,可以看到该模式下,写数据和将数据输出到输出总线上是同时进行的。在向地址 1 和地址 2 写入数据的同时,在对应时刻,经过一个周期延迟后,仍然正确地输出了地址 1 和地址 2 中,但注意,输出的是刚写入的新值。
最后是 read first 读优先模式,从时序上来看,读优先模式和写优先模式类似,同样接收输入的同时,按照时序将数据输出,但不同在于,写优先模式输出的是写入的新值,而读优先模式输出的是在写操作发生之前的旧值,这种不同,实际上是通过将旧值缓存实现的。下图中 0xbbbb,0xcccc 分别为地址 0x1,0x2 中原有的旧值,在向地址 0x1,0x2 写入新值,它们在dout 上被输出。
read first 模式
工作模式总结
实际上,一般不会选择 no change 模式,因为会有错误读取的可能性。根据需求选择 write/read first 即可。当然,最好还是避免冲突访问。
关于文档
岔开下话题,讲讲作者在写文章之前或者使用 ip 核遇到问题时,是怎么解决的。其实无他,你一方面可以 bing 一下,找到类似本文的讲解性文章,试图解开疑问。或者,你可以通过 ip 核的官方手册了解到一些 ip 核的基本特性。在 ip 核配置界面,选择左上方:
可以调转到 DocNav 中对应的手册,如果软件中打不开的话,就在手册的图标上右键,选择在浏览器中打开。open docu in Browser
一般在 PG Product Guide 中,看一下第一部分的概述和第四部分的 ip 核配置界面参数即可,可以之后有问题了,再回去细看别的部分。
当然都是英文的,这没什么办法,可以借助一些翻译工具,有道之类,据说还挺好用的。
结束语
本文从 RAM 开始,简单介绍了各项概念,并介绍了 BRAM ip 核配置的部分参数与选项。通过仿真对单端 RAM 读延迟,使能以及读写冲突情况下的工作模式的验证与学习,末了,简单讨论了翻阅 PG 的一点儿经验。希望能对读者有帮助,有任何问题,我们评论区见。
在后续的文章中,会继续讨论 RAM 的实际使用以及双端口 RAM 的仿真。
参考资料
推荐阅读
关注此系列,请关注专栏FPGA的逻辑