目录
一: RAM 简介
1.1 存储器的分类
在了解 RAM IP 核之前,我们先来看下存储器的大致分类,如下图所示:
由上图可知,存储器包括随机存储器和只读存储器,随机存储器包括静态
RAM
和动态
RAM
。静态
RAM只要有供电,它保存的数据就不会丢失;而动态
RAM
在供电的情况下,还需要根据其要求的时间来对存储
的数据进行刷新,才能保持存储的数据不会丢失。
静态 RAM
一般包括
单端口 RAM
、
简单双端口RAM
和
真双端口 RAM。
静态
RAM 的特点是存储容量相对不是很大,但是读写速度非常高,
动态 RAM
一般包括
SDRAM
和
DDR SDRAM
。目前
DDR SDRAM
已经从
DDR1
代发展到
DDR5
代了,
DDR3
和
DDR4 SDRAM
是目前非常主流的存储器,
其特
点是存储容量非常大、但是读写速度相比于静态
RAM
会稍低一些
。
只读存储器
一般包括 PROM
、
EPROM
和
EEPROM
等,是非易失性的存储器。目前使用率较高的是
EEPROM
,其特点是容量相对较小,存储的一般是器件的配置参数信息,
本次我们学习的
RAM
属于
静态 RAM
,我们重点看下几种静态
RAM
的特性与区别:
不同的特性决定不同的应用场景,在 RAM 的实际应用中,我们一般根据功能需求和带宽需求来选择合适的 RAM 类型,
二: 单端口ram配置
Vivado 软件自带的
Block Memory Generator IP
核
,可以用来配置生成
RAM
或者
ROM
。
RAM
是一种随机存取存储器,不仅可以读出存储的数据,同时还支持对存储
的数据进行修改,而
ROM
是一种只读存储器,也就是说,在工作时只能读出数据,而不能写入数据。需要
注意的是,配置生成的
RAM
或者
ROM
使用的都是
FPGA
内部的
BRAM
资源(
Block RAM
,即块随机存
储器,是
FPGA
厂商在逻辑资源之外,给
FPGA
加入的专用
RAM
块资源),只不过配置成
ROM
时只用到
了嵌入式
BRAM
的读数据端口。本章我们主要介绍如何将
BMG IP
核配置成
RAM
。
2.1 单端口 RAM 的框图
各个端口的功能描述如下:
2.2 RAM IP 核配置
关于具体每一张图中的具体选项的含义可详见正点原子的《领航者ZYNQ 之 FPGA 开发指南》P542
本次实验写入32个数据所以深度为32,数据位宽为8
至此,ramIP核配置成功。生成ramIP核文件。
2.3 RAM 读写模块设计
本次实验任务是在1~31个地址中写入1~31个数,然后再在1~31个地址中读取这些数据。
读写波形图如下图所示
RAM 读写模块设计代码(代码的输入是RAMIP核的输出,代码的输出是RAMIP核的输入)
module ram_rw(
input clk, //系统时钟,50MHz
input reset_n, //系统复位按键,低电平有效
input [7 : 0] ram_rd_data, //ram读数据
output reg ram_en, //ram端口使能信号,高有效
output wire ram_we, //ram读写使能信号,1为写,0为读
output reg [4 : 0] ram_addr, //ram读写地址
output reg [7 : 0] ram_wr_data //ram写数据
);
reg [5:0] rw_cnt ; //读写控制计数器
always @(posedge clk or negedge reset_n)
if(!reset_n)
ram_en <= 0;
else
ram_en <= 1;
assign ram_we = (rw_cnt <= 6'd31 && ram_en == 1'b1) ? 1'b1 : 1'b0;//组合逻辑实现会和ram_en同步
//时序逻辑am_we实现会使得其不在ram_en信号拉高的同时立马拉高,会晚半拍
/*always @(posedge clk or negedge reset_n)
if(!reset_n)
ram_we <= 0;
else if(ram_en && rw_cnt <= 31)
ram_we <= 1;
else
ram_we <= 0;
*/
//读写控制计数器,计数器范围 0~63
always @(posedge clk or negedge reset_n)
if(!reset_n)
rw_cnt <= 0;
else if(ram_en)begin
if(rw_cnt >= 63 )
rw_cnt <= 0;
else
rw_cnt <= rw_cnt + 1;
end
else
rw_cnt <= 0;
//读写地址信号 范围:0~31
always @(posedge clk or negedge reset_n) begin
if(!reset_n)
ram_addr <= 0;
else if(ram_addr == 5'd31 && ram_en)
ram_addr <= 5'b0;
else if (ram_en)
ram_addr <= ram_addr + 1'b1;
else
ram_addr <= 5'b0;
end
//在 WE 拉高期间产生 RAM 写数据,变化范围是 0~31
always @(posedge clk or negedge reset_n) begin
if(!reset_n)
ram_wr_data <= 8'b0;
else if(ram_wr_data < 8'd31 && ram_we)
ram_wr_data <= ram_wr_data + 1'b1;
else
ram_wr_data <= 8'b0 ;
end
endmodule
2.4 顶层模块设计
设计代码
module zdyz_ip_ram(
input clk,
input reset_n
);
wire ram_en;
wire ram_we;
wire [4 : 0] ram_addr;
wire [7 : 0] ram_wr_data ;
wire [7 : 0] ram_rd_data;
//例化ram 读写模块
ram_rw ram_rw(
. clk(clk), //系统时钟,50MHz
. reset_n(reset_n), //系统复位按键,低电平有效
. ram_rd_data(ram_rd_data), //ram读数据
. ram_en(ram_en), //ram端口使能信号,高有效
. ram_we(ram_we), //ram读写使能信号,1为写,0为读
. ram_addr(ram_addr), //ram读写地址
. ram_wr_data(ram_wr_data) //ram写数据
);
blk_mem_gen_0 blk_mem_gen_0 (
.clka(clk), // input wire clka
.ena(ram_en), // input wire ena
.wea(ram_we), // input wire [0 : 0] wea
.addra(ram_addr), // input wire [4 : 0] addra
.dina(ram_wr_data), // input wire [7 : 0] dina
.douta(ram_rd_data) // output wire [7 : 0] douta
);
endmodule
2.5 仿真测试文件代码
`timescale 1ns / 1ps
module zdyz_ip_ram_tb();
reg clk;
reg reset_n;
initial begin
clk = 1'b0;
reset_n = 1'b0;
#200
reset_n = 1'b1;
end
//产生时钟
always #20 clk = ~clk;
zdyz_ip_ram zdyz_ip_ram(
.clk(clk),
.reset_n(reset_n)
);
endmodule
2.6 仿真结果
仿真通过
三:伪双端口配置(小梅哥)
关于具体每一张图中的具体选项的含义可详见小梅哥的《基于HDL的FPGA逻辑设计与验证教程》P314
3.1 伪双端口框图
与单端口 RAM 不同的是,伪双端口 RAM 输入有两路时钟信号 CLKA/CLKB;独立的两组地址信号ADDRA/ADDRB;Port A 仅提供 DINA 写数据总线,作为数据的写入口;Port B 仅提供数据读的功能,读出的数据为 DOUTB。
关于各个引脚说明可参考小梅哥或者正点原子文档教程。
3.2详细配置流程图
选择 Block Memory Generator 双击鼠标进入到 RAM IP 配置界面。
端口类型的选择,Xilinx 的很多 IP 一般都有提供两种接口,一种是常规接口,一种是 AXI 接口,这里选择选择常规接口 Native
。
这里我们选择简单双端口 RAM(Simple Dual Port RAM)
ECC 全称是 Error Correction Capability,是在简单双端口 RAM 类型下的一种纠错功能,具体该功能的详细说明,可以查看 IP 手册,这里选择 NO ECC。
写数据字节使能,如果勾选,写使能信号会根据写数据的字节数生成对应的 bit 数据,1 个字节对应 1bit 写使能,这里字节的大小可以设置为 8 或 9,当这里选择后,输入输出的数据的位宽就必须是 8 或 9 的整数倍,这里我们需要一个位宽为 8bit 的 RAM,这里勾选 Write Enable 并设置字节大小为 8bit。
这里我们保持默认的最小面积选项即可。
RAM 数据位宽和深度设置,这个根据实际应用需求进行设置,这里设置数据位宽 8bit,深度 256。
这里选择NO Change(其他选项的具体说明可参考文档教程)
端口使能信号类型设置,一个是一直使能,一个是通过一个 ENA 信号管脚控制,这里选择 Always Enable。
由于我们前面选择的是简单双端口 RAM,对于端口 A 只能进行数据的写入,没有数据的输出,所有关于端口 A 的数据输出的相关配置是不可配置的。
端口 B 数据位宽和内存深度的设置,这里设置位宽为8,深度会自动根据你选择的位宽进行设置。
端口
B
操作模式不可设置,由于在简单双端口
RAM
下端口
B
只能进行读
操作,不能进行写操作,所以这里不可设置,在真双单口
RAM
下,这里是可进
行设置的。端口使能就设置为
Always Enable
,让端口
B
一直使能。
端口 B
输出寄存器配置,这里可以看下
RAM
内部结构图,可以很清楚的看到
Primitives Output Register
是结构中的
1
处的寄存器,
Core Output Register
是结
构图中
2
出的寄存器。
REGCEB Pin
是寄存器使能管脚,如果勾选,会有一个寄
存器使能控制管脚用于控制寄存器的使能,如果不勾选寄存器就一直使能状态,
这里就不勾选。要得到更好的性能,将这里的两个寄存器都勾选。
端口 B 输出置位/复位设置,这里不创建置位/复位端口,需注意这里置位/复位并不复位 RAM 中的数据而是只复位寄存器上的值。
其他设置,这里不对 RAM 进行初始化, 关于仿真设置就保持默认即可。
3.2 激励文件设计代码
为了测试简单双端口 RAM
,可以通过实际写入一些数据再读取部分数据的
方式来验证双端口
RAM
读写是否正常。添加并新建
tb
文件命名为
ram_tb.v
。编
写
tb
代码,具体
tb
代码实现的是在地址从
0~16
上写入数据为从
255
减至
240
。
延时一段时间后读地址为
0~16
上的数据。
`timescale 1ns / 1ps
module xmg_ram_ip_tb();
reg clka ;
reg clkb ;
reg wea ;
reg [7 : 0] addra ;
reg [7 : 0] dina ;
reg [7 : 0] addrb ;
wire [7 : 0] doutb ;
integer i;//integer类型用于表示整数值。在FPGA设计中,integer类型通常用于计数器、延时器等电路中。作用:用于表示整数。
blk_mem_gen_0 blk_mem_gen_0 (
.clka(clka), // input wire clka
.wea(wea), // input wire [0 : 0] wea
.addra(addra), // input wire [7 : 0] addra
.dina(dina), // input wire [7 : 0] dina
.clkb(clkb), // input wire clkb
.addrb(addrb), // input wire [7 : 0] addrb
.doutb(doutb) // output wire [7 : 0] doutb
);
initial clka = 1;
always #10 clka = ~clka;
initial clkb = 1;
always #10 clkb = ~clkb;
initial begin
wea=0;
addra=0;
dina=0;
addrb=0; //255
#201;
wea = 1;
for (i=0;i<=15;i=i+1)begin
dina=255-i;//写入数据
addra = i;//选择地址
#20;
end
wea=0;
#1;
for (i=0;i<=15;i=i+1)begin
addrb=i;//读取相应地址的数据
#40;
end
#200;
$stop;
end
endmodule
3.3 仿真结果
至此伪双端口RAM配置以及设计仿真完毕。
四:伪双端口配置(正点原子)
由于伪双端口配置较为常用,所以再次再次重复编写一下伪双端口实例。
详细配置方案可参考正点原子文档教程《
领航者ZYNQ 之 FPGA 开发指南》P564。
本节任务是在0~63个地址依次写入0~63个数,然后在写到一半数据(32个数据)时就开始从地址0~63依次读数据
4.1 RAM 写模块设计
模块框图:
模块代码
module zdyz_ram_wr(
input clk , //时钟信号
input reset_n , //复位信号,低电平有效
//RAM 写端口操作
output ram_wr_we , //ram 写使能
output reg ram_wr_en , //端口使能
output reg [5:0] ram_wr_addr , //ram 写地址
output [7:0] ram_wr_data ,//ram 写数据
output reg rd_flag //读启动信号
);
//ram_wr_we 为高电平表示写数据
assign ram_wr_we = ram_wr_en;
//写数据与写地址相同,因位宽不等,所以高位补 0
assign ram_wr_data = {2'b0,ram_wr_addr};
//控制 RAM 使能信号
always @(posedge clk or negedge reset_n)
if(!reset_n)
ram_wr_en <= 1'b0;
else
ram_wr_en <= 1'b1;
//写地址信号 范围:0~63
always @(posedge clk or negedge reset_n)
if(!reset_n)
ram_wr_addr <= 0;
else if(ram_wr_en && ram_wr_addr < 63)
ram_wr_addr <= ram_wr_addr + 1;
else
ram_wr_addr <= 0;
//当写入 32 个数据(0~31)后,拉高读启动信号
always @(posedge clk or negedge reset_n)
if(!reset_n)
rd_flag <= 0;
else if(ram_wr_addr == 31)
rd_flag <= 1;
else
rd_flag <= rd_flag;
endmodule
4.2 RAM 读模块设计
模块框图:
模块代码
module zdyz_ram_rd(
input clk , //时钟信号
input reset_n , //复位信号,低电平有效
//RAM 读端口操作
input rd_flag , //读启动标志
input [7:0] ram_rd_data ,//ram 读数据
output wire ram_rd_en , //端口使能
output reg [5:0] ram_rd_addr //ram 读地址
);
assign ram_rd_en = rd_flag;
//读地址信号 范围:0~63
always @(posedge clk or negedge reset_n)
if(!reset_n)
ram_rd_addr <= 0;
else if(rd_flag && ram_rd_addr < 63)
ram_rd_addr <= ram_rd_addr + 1;
else
ram_rd_addr <= 0;
endmodule
4.3 顶层文件设计
代码设计
module zdyz_ip_2port_ram(
input clk , //系统时钟
input reset_n //系统复位,低电平有效
);
wire ram_wr_we;
wire ram_wr_en;
wire [5:0]ram_wr_addr; //ram 写地址
wire [7:0]ram_wr_data; //ram 写数据
wire [5:0]ram_rd_addr; //ram 读地址
wire [7:0]ram_rd_data; //ram 读数据
wire rd_flag; //读启动标志
wire ram_rd_en;
//RAM 写模块例化
zdyz_ram_wr zdyz_ram_wr(
.clk (clk) , //时钟信号
.reset_n (reset_n) , //复位信号,低电平有效
.ram_wr_we (ram_wr_we) , //ram 写使能
.ram_wr_en (ram_wr_en) , //端口使能
.ram_wr_addr (ram_wr_addr) , //ram 写地址
.ram_wr_data (ram_wr_data) , //ram 写数据
.rd_flag (rd_flag) //读启动信号
);
//RAM 读模块例化
zdyz_ram_rd zdyz_ram_rd(
.clk (clk) ,//时钟信号
.reset_n (reset_n) ,//复位信号,低电平有效
.rd_flag (rd_flag) ,//读启动标志
.ram_rd_data (ram_rd_data) ,//ram 读数据
.ram_rd_en (ram_rd_en) ,//端口使能
.ram_rd_addr (ram_rd_addr) //ram 读地址
);
//RAM IP核例化
zdyz_blk_mem_gen_2 your_instance_name (
.clka(clk), // input wire clka
.ena(ram_wr_en), // input wire ena
.wea(ram_wr_we), // input wire [0 : 0] wea
.addra(ram_wr_addr), // input wire [5 : 0] addra
.dina(ram_wr_data), // input wire [7 : 0] dina
.clkb(clk), // input wire clkb
.enb(ram_rd_en), // input wire enb
.addrb(ram_rd_addr), // input wire [5 : 0] addrb
.doutb(ram_rd_data) // output wire [7 : 0] doutb
);
endmodule
4.4 仿真文件
`timescale 1ns / 1ps
module zdyz_ip_2port_ram_tb();
//parameter define
parameter CLK_PERIOD = 20; //时钟周期 20ns
//reg define
reg clk;
reg reset_n;
//信号初始化
initial begin
clk = 1'b0;
reset_n = 1'b0;
#200
reset_n = 1'b1;
end
//产生时钟
always #(CLK_PERIOD/2) clk = ~clk;
zdyz_ip_2port_ram zdyz_ip_2port_ram(
.clk (clk ),
.reset_n(reset_n)
);
endmodule
4.5 仿真结果
至此RAM IP核配置以及仿真验证已经结束。