【RK3568+PG2L50H开发板实验例程】FPGA部分 | ROM、RAM、FIFO 的使用
本原创文章由深圳市小眼睛科技有限公司创作,版权归本公司所有,如需转载,需授权并注明出处(www.meyesemi.com)
1.实验简介
实验目的:
掌握紫光平台的 RAM、ROM、FIFO IP 的使用
实验环境: Window11 PDS2022.2-SP6.4
芯片型号: PG2L50H-484
2.实验原理
不管是 Logos 系列或者是 Logos2 系列,其 IP 配置以及模式和功能均一致,不会像 PLL 那样有动态配置以及内部反馈选项的选择等之间的差异,所以是 RAM、ROM、FIFO 是通用的。
2.1. RAM 介绍
RAM 即随机存取存储器。它可以在运行过程中把数据写进任意地址,也可以把数据从任意地址中读出。其作用可以拿来做数据缓存,也可以跨时钟,也可以存放算法中间的运算结果等。
注意,PDS的IP配置工具中提供两种不同的RAM,一种是Distributed RAM(分布式RAM) 另一种是 DRM Based RAM,分布式 RAM 用的是 LUT(查找表)资源去构成的 RAM,这种 RAM 会消耗大量 LUT 资源,因此通常在一些比较小的存储才会用到这种 RAM,以节省 DRM 资源。而 DRM Based RAM 是利用片内的 DRM 资源去构成的 RAM,不占用逻辑资源,而且速度快, 通常设计中均使用 DRM Based RAM。
RAM 分为三种,如下表所示:
RAM类型 | 特点 |
单端口 RAM | 只有一个端口可以读写,只有一个读写口和地址口 |
伪双端口 RAM | 有 wr 和 rd 两个端口,顾名思义,wr 只能写,rd 只能读 |
真双端口 RAM | 提供 A 和 B 两个端口,两个端口均可以独立进行读写 |
注意,当使用真双端口时,要避免出现同时读写同个地址,这会造成写入失败,在逻辑设计上需要避开这个情况。
以下给出比较常用的 RAM 的配置作为介绍,通常我们比较常用伪双端口 RAM 来设计,
如图所示:
下图为 IP 配置:
注意,如果勾选 Enable Output Register(输出寄存),输出数据会延迟一个时钟周期。具体每个端口的含义这里参考官方手册,大家也可以自行查看 IP 手册,如下图所示:
DRM Resource Type:用于配置所建 RAM IP 核用的是哪种资源,不同芯片型号可选资源是不一样的,有的是 9K,有的是 18K,有的是 36K,如果没有特殊情况,直接 AUTO 即可。
2.1.1. RAM 的读写时序
配置成不同模式的时候,RAM 的读写时序是不一样的,真双端口和单端口的 RAM 配置均有三种模式,而伪双端口只有一种。由于真双端口和单端口的配置是一样的,这里以真双端口为例子。
分为 NORMAL_WRITE( 正 常 模 式 ) 、 TRANSPARENT_WRITE( 直 写 ) 、 READ_BEFORE_WRITE(读优先模式)三种模式。
而伪双端口不属于上面三种模式,有它独特的模式。这几种模式的差异就在于读写时序的不同,接下来,我们来分析读写时序。
以下时序图均来自官方 IP 手册,并且均未使能输出寄存。注意 wr_en 为 1 时表示写数据, 为 0 表示读数据。
2.1.1.1.NORMAL_WRITE
在 NORMAL_WRITE 这种模式下,可以看到,当时钟的上升沿到来,且 clk_en 和 wr_en均为高电平时,就会把数据写到对应的地址里面,如图中的 1 时刻。然后看读数据端口,当wr_en 不为 0 的时候,a_rd_data 一直为 Don’t Care 状态,而当时钟上升沿到来,且 clk_en为高电平,wr_en 为低电平时,a_rd_data 输出当前 a_addr 里的数据,即 Mem(ADDR1)和 ADDR0 里的 D0。
2.1.1.2.READ_BEFORE_WRITE
在 READ_BEFORE_WRITE 这种模式下,可以看到在 1 的时刻,时钟上升沿到来,且clk_en 和 wr_en 均为高电平,D0 写进了 ADDR0 里面,但是注意看此时的 a_rd_data 和a_addr,可以发现,此时 a_wr_en 并不为 0,可 a_rd_data 还是输出了上一刻 ADDR0 的数据(因为不是输出 D0)。之后,a_wr_en 拉低,此时才是读数据,在 3 时刻,把 ADDR0 的数据读出来,a_rd_data 才输出了 D0。
所以总结一下,这个模式其实就是进行写操作时,读端口会把当前写的地址的原始数据输出,因此叫读优先模式很合情合理对吧,顾名思义,就是我优先把原来的数据读出来。
2.1.1.3.Transparent_Write
在 Transparent_Write 这种模式下,可以看到在 1 的时刻,时钟上升沿到来, 且clk_en 和 wr_en 均为高电平,D0 写进了 ADDR0 里面,但是注意看此时的 a_rd_data 和a_addr,可以发现,此时 a_wr_en 并不为 0,可 a_rd_data 居然直接输出了 D0,之后 a_wr_en拉低,进入读状态,在 2 时刻,再一次把 ADDR0 的数据读出来,输出了 D0。
分析总结一下,根据 1 时刻的情况,我们可以得出结论,在这种模式下,当我们进行写操作时,读端口会马上输出我们写入的数据。所以叫直写模式。
2.1.1.4. 伪双端口的读写时序
注意:wr_en 为 1 时是写操作,为 0 是读操作。
伪双端口的读写时序与上面三种都不同,我们看图 8 的时序来分析:
注意看 1 时刻,此时 wr_en 和 wr_clk_en 均为高电平,所以是写操作,所以 1 时刻就是往地址 ADDR0 里写入 D0,注意此时的 rd_addr 和 rd_data,可以看到这一时刻 rd_addr 是 ADDR2,然后进行写操作时,rd_data 同样输出了 ADDR2 里的数据,而此时 wr_en 还是高电平。接下来看 2 和 3 时刻,此时 wr_en 为 0,rd_clk_en 是高电平,所以是读操作,此时分别读出 ADDR1 和 ADDR0 里的数据,之后 rd_clk_en 变成低电平,读时钟无效,可以看到rd_data 保持 D0 输出。
分析总结一下,主要是 1 时刻,大家可以看到 1 时刻往 ADDR0 写入了 D0,读端口却输出了 ADDR2 中的数据。仔细观察可以得出结论:伪双端口 RAM 在进行写操作的时候,会把当前读端口指向的地址的数据输出。是不是有点像直写?只不过直写是输出写入的数据,而伪双端口是输出读端口指向的地址的数据。
具体大家可以结合视频讲解。
2.2 ROM 介绍
ROM 即只读存储器,在程序的运行过程中他只能被读取,无法被写入,因此我们应该在初始化的时候就给他配置初值,一般是在生成 IP 的时候通过导入.dat 文件对其进行初值配置。
注意,PDS的 IP配置工具中提供两种不同的 ROM,一种是 Distributed ROM(分布式 ROM)另一种是 DRM Based ROM,分布式 ROM 用的是 LUT(查找表)资源去构成的 ROM,这种 ROM会消耗大量 LUT 资源,因此通常在一些比较小的存储才会用到这种 RAM,以节省 DRM 资源。而 DRM Based ROM 是利用片内的 DRM 资源去构成的 ROM,不占用逻辑资源,而且速度快,通常设计中均使用 DRM Based ROM。
以下给出比较常用的 ROM 的配置作为介绍,由于只能读,因此其均为单端口 ROM 如图所示:
下图为 IP 配置:
注意,如果勾选 Enable Output Register(输出寄存),输出数据会延迟一个时钟周期。
同时,可以看到 Enable Init 选项是默认勾选的,并且不可取消。
导入的数据的格式只能为二进制或者是十六进制,demo 选择十六进制。
具体每个端口的含义这里参考官方手册,大家也可以自行查看 IP 手册,如图所示:
可以看到图中给出的是完整的接口列表,一般我们只需要 addr、rd_data、clk、rst这四个信号即可。
以下时序图均来自官方 IP 手册,并且均未使能输出寄存。
2.2.1. ROM 的读时序
可以看到该时序是非常简单的,比如在 TI 时刻,当 clk 上升沿到来时,且 clk_en 为高电平时,给出要读出的地址,rd_data 就会输出数据,在不勾选输出使能寄存的情况下,rd_data的输出会有延迟,具体时间可以从仿真里看到,所以我们在下个时钟周期的上升沿即 T2 时刻的上升沿才能获取到 ROM 读出的值。
所以整体时序非常简单,如果勾选了 clk_en 信号,就要给 clke_en 高电平才能读数据,如果不勾选 clk_en 信号,就一直根据地址读取 ROM 数据。
2.3. FIFO 介绍
FIFO 即先入先出,在 FPGA 中,FIFO 的作用就是对存储进来的数据具有一个先入先出特性的一个缓存器,经常用作数据缓存或者进行数据跨时钟域传输。FIFO 和 RAM 最大的区别就是 FIFO 不需要地址,采用的是顺序写入,顺序读出。
在紫光的 IP 工具中又分为 Distribute FIFO 和 DRM FIFO,其实就是用不同的资源去构成,前者 Distribute FIFO 也就是分布式 FIFO,使用的是片上的 LUT 资源去构成,而 DRM FIFO 使用的是片上的 DRM 资源去构成,DRM 构成的 FIFO 其性能大于 LUT 资源构成的,不仅容量更大,且可配置更多功能。
本章着重介绍 DRM Based FIFO。
注意:FIFO 写满后禁止继续写入数据,否则将会写溢出。
注意:FIFO 读空后禁止继续读数据,否则将会读溢出。
以下给出常用的 FIFO 的配置作为介绍。
注意,如果勾选 Enable Output Register(输出寄存),输出数据会延迟一个时钟周期。
FIFO Type 有 SYNC 和 ASYNC 两种,第一种是同步 FIFO,读写端口共用一个时钟和复位,另一种是异步 FIFO,读写时钟和复位均独立。在平常设计中,比较常用的是异步 FIFO,因为同步 FIFO 和异步 FIFO 的读写时序一模一样,只有读写端口的时钟复位有差异,当异步 FIFO 的读写端口使用相同的时钟和复位,此时异步 FIFO 和同步 FIFO 基本是一致的。
Reset Type 也可以选择 SYNC 和 ASYNC 两种,SYNC 模式下需要时钟的上升沿采样到复位有效才会复位,而在 ASYNC 模式下,复位一旦有,FIFO 立即复位。
其余端口说明引用官方 IP 手册,如图所示:
其中 rd_water_level 和 wr_water_level 分别代表”可读的数据量”和”已写入的数据量”,其含义与 Xilinx 的 FIFO 的 wr_data_count 和 rd_data_count 是一致的。
当我们将 Enable Almost Full Water Level 和 Enable Almost Empty Water Level 勾选上,才能看到 rd_water_level 和 wr_water_level,而下面的 Almost Full Numbers 的设置是表示当写入 1020 个数据时,Almost Full 信号就会拉高,Almost Empty Numbers 的设置表示当可读数据剩下 4 个时 Almost Empty 信号就会拉高。
同时 FIFO 支持混合位宽,例如写端口 16bit,读端口 8bit。如果写入 16’h0102,那么读出来会是 8’h02,8’h01,会先读出低位。
如果写端口 8bit,读端口 16bit。当写入 8’h01,8’h02 时,读出来是 16’h0201,先写入的数据存放在低位。
2.3.1.FIFO的读写时序
因为同步 FIFO 和异步 FIFO 的读写时序一致,这里用异步 FIFO 的读写时序图来做介绍。
注意:复位时高电平有效。读出数据均未勾选 Enable Output Register(输出寄存)。
2.3.1.1.FIFO 未满时的写时序
可以看到在 1 时刻,复位信号时低电平,处于工作状态,此时在 wr_clk 的上升沿且 wr_en 为高电平时将数据 D0 写入 FIFO,wr_water_level 也从 0 变 1,表示已经写入了一个数据,此时注意看读端口的 empty 信号,在 3 时刻 empty 信号从高变低,意味着读端口已经有数据可以读了,FIFO不再为空,而注意看,rd_clk和 wr_clk是不一样的,从 1写入到 3时刻 empty 拉低时,经过了 3 个 rd_clk。
所以这里我们可以得出结论:rd_water_level 要滞后 wr_water_level 三个 rd_clk。
2.3.1.2.FIFO 将满时的写时序
将满时主要分析 full 和 almost_full 信号。假设 Almost Full Numbers 设置为 N-2,在 1时刻,此时已经写入了 N-6 个数据,意味着再写 6 个数据 FIFO 就满了,从 1 时刻到 2 时刻一共写入了 4 个数据,因此当 wr_water_level 变成 N-2 时,满足条件,可以看到 Almost Full信号拉高,再写两个数据 FIFO 就满了,所以再经过两个时钟周期后,Full 信号拉高。
2.3.1.3.FIFO 在满状态下的读时序
在满状态下,FIFO 已经有 N 个数据了,此时在 1 状态下,rd_clk 的上升沿,且 rd_en 为 高 电 平 时 ,此 时 从 FIFO 里 读 出 数 据 (数 据 的 输 出 有 延 时 ,仿 真 中 延 时 0.2ns) 。此时 rd_water_level 变成 N-1,rd_data 输出 D0。然后看 2 时刻,full 信号拉低,此时可以看以下,在 1 时刻到 2 时刻期间一共经过了 3 个 wr_clk 写端口才能判断到此时数据量已经不为满。
所以我们可以得出结论,wr_water_level 要滞后 rd_water_level 三个 wr_clk。
2.3.1.4.FIFO 将空时的读时序
在 1 时刻,可读的数据量剩下 4,假设 Almost Empty Number 设为 2,在 1 时刻和 2 时刻分别读出了两个数据,所以在 2 时刻下,可读数据量剩下两个,达到 Almost Empty Number 触发条件,因此 almost_empty 信号拉高,再过两个时钟周期,即再读两个数据,FIFO 将变成空状态,也就是状态 3,此时 empty 信号拉高。
2.4. 接口列表
该部分介绍每个顶层模块的接口。
ram_test_top.v
rom_test_top.v
fifo_test_top.v
3.代码仿真说明
本次的顶层模块实际就是例化 IP,然后把端口引出而已,主要代码都在 testbench 里面,所以我们直接介绍仿真代码。
3.1. RAM 仿真测试
`timescale 1ns/1nsmodule ram_test_tb();reg sys_clk;reg rd_clk;reg rst_n;reg rw_en; //读写使能信号reg [7:0] wr_data;reg [4:0] wr_addr;reg [4:0] rd_addr;wire [7:0] rd_data;reg [1:0] state;initial beginrst_n <= 1'd0;sys_clk <= 1'd0;rd_clk <= 1'd0;#20rst_n <= 1'd1;end//读写控制always@(posedge sys_clk or negedge rst_n) beginif(!rst_n) beginstate <= 2'd0;wr_data <= 8'd0;rw_en <= 1'd0;wr_addr <= 8'd0;rd_addr <= 8'd0;endelse begincase(state)2'd0: beginrw_en <= 1'd1;state <= 2'd1;end2'd1: beginif(wr_addr == 5'd31) beginrw_en <= 1'd0;state <= 2'd2;wr_data <= 8'd0;wr_addr <= 5'd0;rd_addr <= 5'd0;endelse beginstate <= 2'd1;wr_data <= wr_data+1'b1;rd_addr <= rd_addr+1'b1;wr_addr <= wr_addr+1'b1;endend2'd2: beginif(rd_addr == 5'd31) beginstate <= 2'd3;rd_addr <= 5'd0;endelse beginstate <= 2'd2;rd_addr <= rd_addr+1'b1;endend2'd3: beginstate <= 2'd0;enddefault: state <= 2'd0;endcaseendend//50MHZalways #10 sys_clk = ~sys_clk;GTP_GRS GRS_INST(.GRS_N(1'b1));ram_test_top u_ram_test_top(.wr_clk (sys_clk),.rd_clk (sys_clk),.rst_n (rst_n),.rw_en (rw_en),.wr_addr (wr_addr),.rd_addr (rd_addr),.wr_data (wr_data),.rd_data (rd_data));
endmodule
涉及到 tb 的一些基础操作这里就不再详细讲解,只关注重点逻辑部分。从代码的 27 行到 80 行是 ram 的读写控制状态机。主要用来控制读写地址的生成和使能以及写入的数据。这里只讲解主要实现的功能,首先代码的 38-42 行,也就是 state=0 的时候,拉高 rw_en,并跳转到状态 1,此时进入写操作(没有使能 clk_en,可以不管),下个时钟周期开始写入数据(注意是时序逻辑,边沿采样,所以是下个时钟周期才开始写数据),即 state=1 的时候是一直在往 ram 里面写数据,在代码的 44 到 60 行就是写操作了,可以看到,当 wr_addr 不等于 31 的时候,wr_data 和 wr_addr 不断加 1(rd_addr 这里+1,可以看视频讲解,主要为了验证伪双端口的时序),当 wr_addr 等于 31 的时候,在下个时钟周期把数据清 0,状态跳转,在当前时钟周期下还会再往地址 31 里面写入数据,所以在该时钟周期,一共写入了 32 个数据(从地址 0 写到地址 31)。即状态 1 完成写入 32 个数据后跳转到 state=2 的逻辑。代码的 61-72 行,也就是 state=2 的时候,在每个周期的上升沿让 rd_addr 不断累加,直到 rd_addr=31 的时候,在下个时钟周期清空地址并让状态跳转的操作,而在当前时钟周期会继续把地址 31 的数据读出来,完成读取地址 0-31 的数据,一共 32 个数据,所以该状态主要完成读取 32 个数据,然后在下个时钟周期就跳转到 state=3。state=3 可以看到其主要作用就是等待一个时钟周期,然后跳转回去 state=0 下,起到一个延时作用。
上图为写数据的波形,数据从 0 开始递增到 31,地址也是从 0 到 31。
上图为读数据波形,从地址 0-31 读出了 0-31 个数据。
具体波形大家可以看视频仿真,或者自己尝试仿真,根据波形来看代码。因为这里是时序逻辑,所以如果是初学者,纯看文字可能会对 rd_addr=31 这一时刻还会再读一个数据感到疑惑,建议直接仿真,或者观看视频讲解的仿真部分,可以帮助快速理解。
可以总结出一句话就是时序逻辑的赋值总在下一个时钟周期才生效。所以在rd_addr=31 时执行的操作要在下一个时钟周期才会被采样生效。所以当前时钟还是会再从RAM 读出一个数据。
仿真代码的讲解到此结束,大家要注意时序逻辑的特点,具体的内容请看视频讲解。
3.2. ROM 仿真测试
`timescale 1ns/1nsmodule rom_test_tb();reg sys_clk;reg rst_n;reg [9:0] rd_addr;wire [63:0] rd_data;initial beginrst_n <= 1'd0;sys_clk <= 1'd0;#20rst_n <= 1'd1;end//50MHZalways #10 sys_clk = ~sys_clk;GTP_GRS GRS_INST(.GRS_N(1'b1));always@(posedge sys_clk or negedge rst_n) beginif(!rst_n)rd_addr <= 10'd0;elserd_addr <= #2 rd_addr + 1'b1;endrom_test_top u_rom_test_top(.rd_clk (sys_clk),.rst_n (rst_n),.rd_addr (rd_addr),.rd_data (rd_data));endmodule
代码 31-36 行例化了 ROM 的顶层模块,该模块里面其实就是调用了 ROM IP,然后把信号引出端口,没有任何逻辑操作。
代码 24-29 行通过一个 always 块不断生成地址,任何给到 ROM IP,将数据读出,由于没勾选 clk_en 信号,所以数据在 ROM 复位完成后就会不断读出。所以并没有复杂的逻辑,就是让地址从 0 不断累加,把数据读出。
上图为读出数据的波形图,可以看到读出的数据和 dat 文件里的数据一致。
仿真代码的讲解到此结束,大家要注意时序逻辑的特点,具体的内容请看视频讲解。
3.3. FIFO 仿真测试
`timescale 1ns/1nsmodule fifo_test_tb();reg sys_clk;reg rst_n;reg [7:0] wr_data;reg wr_en;reg rd_en;reg rd_state; //读状态reg wr_state;wire [7:0] rd_data;reg [7:0] rd_cnt;wire [7:0] rd_water_level;wire [7:0] wr_water_level;initial beginrst_n <= 1'd0;sys_clk <= 1'd0;#20rst_n <= 1'd1;endalways #10 sys_clk = ~sys_clk; //50MHZalways@(posedge sys_clk or negedge rst_n) beginif(!rst_n) beginwr_state <= 1'd0;wr_en <= 1'd0;wr_data <= 8'd0;endelse begincase(wr_state)1'd0: if(wr_water_level == 127) //128 个数据beginwr_en <= #2 1'd0;wr_data <= #2 8'd0;wr_state <= #2 1'd1;endelsebeginwr_en <= #2 1'd1;wr_data <= #2 wr_data+1'b1;wr_state <= #2 1'd0;end1'd1: if(rd_cnt == 127)wr_state <= #2 1'd0;default: wr_state <= 1'd0;endcaseendendalways@(posedge sys_clk or negedge rst_n) beginif(!rst_n) beginrd_state <= 1'd0;rd_en <= 1'd0;rd_cnt <= 8'd0;endelse begincase(rd_state)1'd0: if(rd_water_level >= 8'd128) //等待 128 个数据beginrd_state <= #2 1'd1;rd_en <= #2 1'd1;endelsebeginrd_cnt <= #2 8'd0;rd_state <= #2 1'd0;end1'd1: beginrd_cnt <= #2 rd_cnt + 1'b1;if(rd_cnt == 127)beginrd_en <= #2 1'd0;rd_state <= #2 1'd0;endenddefault: rd_state <= 1'd0;endcaseendendGTP_GRS GRS_INST(.GRS_N(1'b1));fifo_test_top u_fifo_test_top(.sys_clk (sys_clk),.rst_n (rst_n),.wr_data (wr_data),.wr_en (wr_en),.rd_en (rd_en),.wr_water_level (wr_water_level),.rd_water_level (rd_water_level),.rd_data (rd_data));
endmodule
涉及到 tb 的一些基础操作这里就不再详细讲解,只关注重点逻辑部分。整个设计分为读写两个状态的控制。分别完成了写入 128 个数据,和读出 128 个数据,由于 FIFO 不需要地址,所以只需要产生使能信号即可。
首先看写状态,在 wr_state=0 时,拉高写使能,并让 wr_data 不断累加,往 FIFO 里面写数据,当 wr_water_level=127 的时候,拉低写使能,写数据置 0,写状态跳转到 1,注意此时还会再写入一个数据,所以到此一个写入了 128 个数据。至于拉低写使能,写数据置 0,写状态跳转到 1 这些操作将在下一个时钟周期才会被采样生效。之后,在 wr_state=1 时,不断等待 rd_cnt,该条件就是判断当读出 128 个数据的时候,wr_state 跳转到 0 状态。
接下来看读状态,在 rd_state=0 的时候,一旦可读的数据量超过 128 个(包括 128),状态跳转到 rd_state=1 下,然后开始读出数据,同时在 rd_state=1 下用变量 rd_cnt 对我们的读出数据也进行计数,rd_cnt 从 0 开始计数,当 rd_cnt=127 的时候会再往 FIFO 读出一个数据,所以此时就一共读出了 128 个数据,下一个时钟周期 rd_en 和 rd_state 都将置 0。
写数据的波形如上所示,一共写入 128 个数据,从 1 写到 128。
读数据的波形如上所示,一共读出 128 个数据,从 1 读到 128。
具体波形大家可以看视频仿真,或者自己尝试仿真,根据波形来看代码。因为这里是时序逻辑,所以如果是初学者,纯看文字可能会对 rd_cnt=127 这一时刻还会再读一个数据感到疑惑,建议直接仿真,或者观看视频讲解的仿真部分,可以帮助快速理解。
可以总结出一句话就是时序逻辑的赋值总在下一个时钟周期才生效,所以在 rd_cnt=127 时执行的操作要在下一个时钟周期才会被采样生效。所以当前时钟 rd_en 还是 为 1,会再从 FIFO 读出一个数据。
仿真代码的讲解到此结束,具体的内容请看视频讲解。