ROM概述
ROM 是只读存储器(Read-Only Memory)的简称,是一种只能读出事先所存数据的固态半导体存储器。其特性是一旦储存资料就无法再将之改变或删除,且资料不会因为电源关闭而消失。
而事实上在 FPGA
中通过
IP
核生成的
ROM
或
RAM
调用的都是 FPGA
内部的
RAM
资源,掉电内容都会丢失(这也很容易解释,
FPGA
芯片内部本来就没有掉电非易失存储器单元)。用 IP
核生成的
ROM
模块只是提前添加了数据文件(.mif
或
.hex
格式),在
FPGA
运行时通过数据文件给
ROM
模块初始化,才使得
ROM
模块像个“真正”的掉电非易失存储器;也正是这个原因,ROM
模块的内容必须提前在数据文件中写死,无法在电路中修改。
Altera
推出的
ROM IP
核分为两种类型:单端口
ROM
和双端口
ROM
。
单端口ROM :提供一个读地址端口和一个读数据端口,只能进行读操作;
双端口
ROM:
与单端口ROM 类似,区别是其提供两个读地址端口和两个读数据端口,基本上可以看做两个单口RAM 拼接而成。
![](https://img-blog.csdnimg.cn/44eba6dbc3c84a3c8d538e7e55ac999c.png)
单端口ROM接口信号
双端口ROM接口信号
单端口ROM的配置
我们在工程目录下新建一个
ip_core
文件夹,将
IP核保存在该文件夹下并命名为 rom_256x8
(
rom
是我们调用的
IP
核,
256
是调用的
IP
核容量,8
是调用的
IP
核数据位宽。这里这样命名是为了方便识别我们创建的
IP
核类型及资源量,我们制作的数据文件就是容量为 256
,数据位宽为 8bit)。
![](https://img-blog.csdnimg.cn/16037e8910584ab8a48f359e8991d25f.png)
选择使用的时钟模式,可选择单时钟或双时钟。选择单时钟时用一个时钟控制存储块的所有寄存器,选择双时钟时输入时钟控制地址寄存器,输出时钟控制数据输出寄存器。ROM 模式没有写使能、字节使能和数据输入寄存器。这里我们选择默认选项Single clock(单时钟)。
上图是选择是否输出“q”寄存器。这里我们把输出端口的寄存器去掉(如果不去掉的 话,就会使输出延迟一拍。如果没有特别的需求的话我们是不需要延迟这一拍的).
不勾选时,数据输出会延时地址输入一个时钟周期,勾选时,时钟信号会输入更高的频率,数据输出会延时地址输入两个时钟周期
上图是添加文件路径
提示了我们单独使用第三方仿真工具时需要添加名为 “altera_mf”的库
双端口ROM的配置
As a number of words”是按字数确定,“AS a number of bits”是按比特数确定。
(注意:选择的容量需大于我们需要写入的数据文件的数据量
Single clock(单时钟):使用一个时钟控制。
Dual clock
:
use separate
“
input
”
and
“
output
”
clocks
(双时钟:使用单独的输入时钟和输出时钟):输入和输出时钟分别控制存储块的数据输入和输出的相关寄存器。
Dual clock
:
use separate clocks for A and B ports
(双时钟:端口
A
和端口
B
使用不同的独立时钟):时钟 A
控制端口
A
的所有寄存器,时钟
B
控制端口
B
的所有寄存器。
ROM IP核的使用
实验目标
将
ROM 内的数据读取出来显示在数码管上。我们 ROM 的初始化数据是 0~255,也就是存入数据
0~255
。然后每隔
0.2s
我们从
0
地址开始往下读取数据显示在数码管上,我们再利用两个按键信号来读取指定地址的数据,每按一个按键就读取一个地址的数据显示在数码管上。再次按下按键后,以当前地址继续以 0.2s
的时间间隔往下读取数据并显示出来。
ROM IP核模块
ROM控制模块
从
ROM
的数据手册可知,读操作是在时钟的上升沿触发的,而我们在调用
ROM
时是没有生成读使能的,所以在
读时钟上升沿我们只要给相应的地址就能在时钟的上升沿读出该地址内的数据了。在该模块我们只需要控制生成读地址即可。
ROM控制模块输入输出信号
ROM
并不是只能从
0 地址开始读取,它能读取指定的任意地址,为了验证这个功能,我们加入两个按键信号,每按一个按键就读出一个指定地址的数据,再次按下就又沿当前地址顺序读取。
addr_flag1
、
addr_flag2
: 读 地 址 的 标 志 信 号 。
当 按 下 按 键 1/ 按 键 2
(key1_flag/key2_flag=1)时,让读地址的标志信号为高,再次按下时让读地址的标志信号为低,我们使用一个取反操作即可完成。
当按键 1
按下后,
addr_flag1
为高读取地址
99
的数据;当按下按键 2
后,
addr_flag2
为高读取地址
199
的数据(地址大家可任意去取,只要在 ROM
的地址范围即可)。这里需要注意的是
因为每次我们只能读取一个地址的信号,所以我们在拉高一个地址标志信号时要将另一个地址标志信号置为 0,这样读取的地址才不会有冲突。
ROM控制模块参考代码(rom_ctrl.v)
module rom_ctrl
#(
parameter CNT_MAX = 24'd9_999_999
)
(
input wire sys_clk ,
input wire sys_rst_n,
input wire key1 , //按键1消抖后有效信号
input wire key2 , //按键2消抖后有效信号
output reg [7:0] addr //输出读ROM地址
);
reg [23:0] cnt_200ms ;
reg key1_en; //特定地址 1 标志信号
reg key2_en; //特定地址 2 标志信号
//0.2s 循环计数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_200ms <= 24'd0;
else if(cnt_200ms == CNT_MAX || key1_en == 1'b1 || key2_en == 1'b1)
cnt_200ms <= 24'd0;
else
cnt_200ms <= cnt_200ms + 1'b1;
//产生特定地址 1 标志信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
key1_en <= 1'b0;
else if(key2 == 1'b1)
key1_en <= 1'b0;
else if(key1 == 1'b1)
key1_en <= ~key1_en;
else
key1_en <= key1_en;
/产生特定地址 2 标志信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
key2_en <= 1'b0;
else if(key1 == 1'b1)
key2_en <= 1'b0;
else if(key2 == 1'b1)
key2_en <= ~key2_en;
else
key2_en <= key2_en;
//让地址从 0~255 循环,其中两个按键控制两个特定地址的跳转
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
addr <= 8'd0;
else if(addr == 8'd255 && cnt_200ms == CNT_MAX)
addr <= 8'd0;
else if(key1_en == 1'b1)
addr <= 8'd99;
else if(key2_en == 1'b1)
addr <=8'd199;
else if(cnt_200ms == CNT_MAX)
addr <= addr + 1'b1;
endmodule
顶层模块
rom
顶层模块主要是对各个子功能模块的实例化,以及对应信号的连接
![](https://img-blog.csdnimg.cn/1f0537d96f7c476090c133abc8ffa7eb.png)
rom
顶层模块主要是对各个子功能模块的实例化,以及对应信号的连接,代码编写较为容易,无需波形图的绘制
module rom
(
input wire sys_clk,
input wire sys_rst_n,
input wire key1 ,
input wire key2 ,
output wire ds,
output wire oe,
output wire shcp,
output wire stcp
);
wire key1_flag; //按键 1 消抖信号
wire key2_flag; //按键 2 消抖信号
wire [7:0] addr;
wire [7:0] data; //读出 ROM 数据
key_filter
#(
.CNT_MAX (20'd999_999)
)
key_filter_inst1
(
.sys_clk (sys_clk),
.sys_rst_n(sys_rst_n),
.key_in (key1),
.key_flag (key1_flag) //key_flag 为 1 时表示消抖后检测到按键被按下
);
key_filter
#(
.CNT_MAX (20'd999_999)
)
key_filter_inst2
(
.sys_clk (sys_clk),
.sys_rst_n(sys_rst_n),
.key_in (key2),
.key_flag (key2_flag) //key_flag 为 1 时表示消抖后检测到按键被按下
);
rom_ctrl
#(
.CNT_MAX (24'd9_999_999)
)
rom_ctrl_inst
(
.sys_clk (sys_clk),
.sys_rst_n(sys_rst_n),
.key1 (key1_flag), //按键 1 消抖后有效信号
.key2 (key2_flag), //按键 2 消抖后有效信号
.addr (addr) //输出读 ROM 地址
);
rom_8x256 rom_8x256_inst
(
.address ( addr ),
.clock ( sys_clk ),
.q ( data )
);
seg_595_dynamic seg_595_dynamic
(
.sys_clk (sys_clk),
.sys_rst_n(sys_rst_n),
.data ({12'b0,data}),
.point (6'b000_000), //小数点显示,高电平有效
.sign (1'b0), //符号位,高电平显示负号
.seg_en (1'b1), //数码管使能信号,高电平有效
.ds (ds ), //串行数据输入
.oe (oe ), //输出使能信号
.shcp (shcp), //移位寄存器的时钟输入
.stcp (stcp) //输出数据存储寄时钟
);
endmodule
RTL视图
ROM IP核仿真
`timescale 1ns/1ns
module tb_rom();
reg sys_clk;
reg sys_rst_n;
reg key1;
reg key2;
wire ds;
wire oe;
wire shcp;
wire stcp;
initial
begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
key1 <= 1'b1;
key2 <= 1'b1;
#20
sys_rst_n <= 1'b1;
#700000 //仿真时是100个时钟周期,一个时钟周期20ns,也就是一个
//数据的显示时间是100*20=2000ns,
//256个数据显示时间是256*2000=512000ns,故延时的时间需要大于此值
//key1
key1 <= 1'b0;
#20
key1 <= 1'b1;
#20
key1 <= 1'b0;
#20
key1 <= 1'b1;
#20
key1 <= 1'b0;
#200 //以上是模拟按键前抖动,以下是模拟按键后抖动
key1 <= 1'b1;
#20
key1 <= 1'b0;
#20
key1 <= 1'b1;
#20
key1 <= 1'b0;
#20
key1 <= 1'b1;
//key2
#20000
key2 <= 1'b0;
#20
key2 <= 1'b1;
#20
key2 <= 1'b0;
#20
key2 <= 1'b1;
#20
key2 <= 1'b0;
#200 //以上是模拟按键前抖动,以下是模拟按键后抖动
key2 <= 1'b1;
#20
key2 <= 1'b0;
#20
key2 <= 1'b1;
#20
key2 <= 1'b0;
#20
key2 <= 1'b1;
//key2
#20000
key2 <= 1'b0;
#20
key2 <= 1'b1;
#20
key2 <= 1'b0;
#20
key2 <= 1'b1;
#20
key2 <= 1'b0;
#200 //以上是模拟按键前抖动,以下是模拟按键后抖动
key2 <= 1'b1;
#20
key2 <= 1'b0;
#20
key2 <= 1'b1;
#20
key2 <= 1'b0;
#20
key2 <= 1'b1;
end
always #10 sys_clk = ~sys_clk;
//defparam rom_inst.key_filter_indt1.CNT_MAX = 9;
rom rom_inst
(
.sys_clk (sys_clk ),
.sys_rst_n(sys_rst_n),
.key1 (key1 ),
.key2 (key2 ),
.ds (ds ),
.oe (oe ),
.shcp (shcp ),
.stcp (stcp )
);
endmodule
接下来仿真验证