Verilog中存储器(寄存器数组)定义、读写、初始化
2021-02-13 16:14阅读:
前言
逻辑电路设计经常会用到单口RAM、双口RAM和ROM等类型的存储器。Verilog中使用数组方式来对存储器进行建模(数组的维数不能大于2)。具体说就是将内存宣称为一个reg类型的数组,这个数组中任何一个单元都可通过一个下标(地址)访问。存储器属于寄存器数组类型,线网数据类型没有相应的存储器类型。FPGA中都有内嵌的RAM资源,可分为块RAM和分布式RAM(一种基于底层逻辑单元,通过查找表和触发器实现的RAM结构),因此在FPGA开发设计过程中,可综合的设计代码中一般不推荐使用Verilog建模RAM,但可在用于仿真的Testbench代码中使用Verilog建模存储器。
一、存储器的定义、读、写
1、存储器的定义方式如下:
1
|
|
reg [wordsize:0]
array_name [0:array_size];
|
比如:
(1)下面定义的my_memmory
为256个8位寄存器的数组
1
|
|
reg [7:0]
my_memmory
[0:255];
|
其中[7:0]是8位内存宽度(1个存储单元占8bit大小空间);[0:255]是256大小的内存深度(也就是有256个存储单元),地址0对应数组中的第0个存储单元。
(2)下面定义的my_memory1为5个1位寄存器的数组(存储器)
单个寄存器说明既能够用于说明寄存器类型,也可以用于说明存储器类型。
1
|
|
reg [1: 8]
my_memory2[ 256 :
0], DataReg;
|
my_memory2是存储器,是16个8位寄存器数组(存储器),而DataReg是8位寄存器。等价于:
1
2
|
|
reg [1: 8]
my_memory2 [ 256 :
0];
reg [1: 8]
DataReg;
|
2、对存储器某个存储单元进行写操作
存储器赋值有别于寄存器赋值:存储器全部存储单元赋值不能在一条赋值语句中完成,但是寄存器可以。因此在存储器被赋值时,需要定义一个索引。
比如,下面对寄存器赋值是正确的:
1
2
|
|
reg [1:5]
DataReg; //DataReg为5位寄存器。 DataReg
= 5'b11011;
|
但下面赋值是不正确的:
1
2
|
|
reg
my_memory1[1:5];
//my_memory1为5个1位寄存器的存储器。 my_memory1
=
5'b11011;//存储器赋值不能在一条赋值语句中完成
|
存储器可用如下方式对某个存储单元进行写操作(也可利用系统任务函数$readmemb、$readmemh对存储器赋值,用法见下面存储器内存初始化部分):
1
|
|
my_memory1[0] = 1'b1;
//my_memory1[address] =
data_in;
|
3、对存储器某个存储单元进行读操作
1
|
|
data_out = my_memmory[address];
|
4、读取存储器中的某1位或者多位
在verilog中,
在使用存储单元时,不能直接引用存储器某个地址的某bit位值,即不允许对存储器读/写某1个bit位,因此若要读1位或多位,需要使用寄存器变量做
中间转换,即需要先将存储单元赋值给某个寄存器,然后在对该寄存器的某bit位进行操作。
1
2
3
4
|
|
reg[7:0]
data_out; //中间寄存器变量
reg data_out_bit0;
data_out = my_memory[address];
data_out_bit0 = data_out[0];
|
二、存储器的初始化
地址一词指对存贮器(memory)建模的数组的寻址指针。当数据文件被读取时,每一个被读取的数字都被存放到地址连续的存贮器单元中去。存贮器单元的存放地址范围由系统任务声明语句中的起始地址和结束地址来说明(也可在内存文件中指定目的地址,达到不连续存储的目的),每个数据的存放地址在数据文件中进行说明。
可用系统任务函数对存储器进行内存初始化操作(系统函数、系统任务都是不可综合的,只用用于TestBench),即使用下面2个系统任务来将保存在文件中的数据填充到内存单元中去(对存储器存储单元的赋值操作)。
- $readmemh(加载十六进制值)
- $readmemb(加载二进制值)
1、$readmemh
(1)用法:
1
2
3
4
|
|
$readmemh('<数据文件名>',<存贮器名>);
$readmemh('<数据文件名>',<存贮器名>,<起始地址>);
$readmemh('<数据文件名>',<存贮器名>,<起始地址>,<结束地址>);
$readmemh('file_name', my_memmory,
start_addr, stop_addr);
|
file_name:包含数据的文本文件名;
my_memory:要初始化的内存单元数组名;
start_addr/stop_addr:可选,指示要初始化单元的起始地址和结束地址。
1)
$readmemh('<数据文件名>',<存贮器名>);
如果系统任务声明语句中和数据文件里都没有进行地址说明,则缺省的存放起始地址为该存贮器定义语句中的起始地址。数据文件中的数据将被连续存放到该存贮器中,直到该存贮器单元存满为止或数据文件里的数据存完。
2)
$readmemh('<数据文件名>',<存贮器名>,<起始地址>);
如果系统任务中说明了存放的起始地址,没有说明存放的结束地址,则数据从起始地址开始存放,存放到该存贮器定义语句中的结束地址为止。
3)
$readmemh('<数据文件名>',<存贮器名>,<起始地址>,<结束地址>);
如果在系统任务声明语句中,起始地址和结束地址都进行了说明,则数据文件里的数据按该起始地址开始存放到存贮器单元中,直到该结束地址,而不考虑该存贮器的定义语句中的起始地址和结束地址。
4)如果地址信息在系统任务和数据文件里都进行了说明,那么数据文件里的地址必须在系统任务中地址参数声明的范围之内。否则将提示错误信息,并且装载数据到存贮器中的操作被中断。
5)如果数据文件里的数据个数和系统任务中起始地址及结束地址暗示的数据个数不同的话,也会提示错误信息。
2、$readmemb
用法与$readmemh类似
3、关于内存文件
在这两个系统任务中,被读取的数据文件的内容有以下约束:
(1)只能包含:空白位置(空格,换行,制表格(tab)和form-feeds),注释行(“//”形式的和“”形式的都允许);
(2)对于$readmemb系统任务,内存文件中国每个数字必须是二进制数字,数字中不能包含位宽说明和格式说明;
(3)对于$readmemh系统任务,每个数字必须是十六进制数字,数字中不能包含位宽说明和格式说明;
(4)数字中不定值x或X,高阻值z或Z,和下划线(_)的使用方法及代表的意义与一般Verilog程序中的用法及意义是一样的;
(5)2数字之间必须用空白位置(空格,换行,制表格(tab))或注释行来分隔开,为便于识别,一般每一行一个数字;
(6)当地址出现在数据文件中,其格式为字符“@”后跟上十六进制数,可以在数据文件里出现多个地址。当系统任务遇到一个地址说明时,系统任务将该地址后的数据存放到存贮器中相应的地址单元中去。对于这个十六进制的地址数中,允许大写和小写的数字。没有@符号就表示地址将顺序递增。
下面是一个内存文件的例子,从文件中可看到,仅初始化8’h00,8’h01,8’h55和8’h56 4个内存地址单元:
1
2
3
4
5
6
|
|
// Comments are allowed
CC
// This is
first address i.e 8'h00 AA
// This is second address
i.e 8'h01 @55
// Jump to new
address 8'h55 5A
// This
is address 8'h55 69
// This is address
8'h56
|
三、关于$readmemb、$readmemh系统任务,有下列常见的用法:
(1)顺序初始化所有的数组单元
这种情况下,可以不使用@符号指示初始化地址,而只在每一行存放要存放的数据。这样数据将从0地址开始,按地址递增顺序存放。
(2)只初始化部分的数组单元
这种情况下,可以使用@符号来指示下一个要初始化的地址,然后对该地址单元进行初始化
(3)只初始化数组的地址区间的一部分单元
这种情况下,使用$readmemh任务的start_addr 和 stop_addr选项来指定初始化的范围。例如:
1
|
|
$readmemh(“memory.list”, my_memory,
100, 104);
//指定使用memory.list来初始化my_memory的100-104存储单元
|
内存文件memory.list可定义为:
四、示例
1、memory.v
Verilog Code
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
|
|
`timescale 1ns/100ps
module Memory(
input
wire
sclk,
input
wire
rst_n,
input
wire
AddEn,
output
reg [7:0]
Memory0
);
parameter ADD_VAL =
1;
parameter MEM_WIDTH =
8;
parameter MEM_DEEPTH =
256;
//定义内存深度为256,宽度为8的存储器 reg
[MEM_WIDTH-1:0] my_memory
[0:MEM_DEEPTH-1];
//使用memory.dat内的数据初始化地址3-10之间的内存
//.dat文件内数据如下,由于第3行数据跳到8'h08地址,因此,最终初始化的地址为(十进制):03,04,08,09,10
initial begin
$readmemh('memory.dat', my_memory,
3, 10);
end
always@(posedge sclk or
negedge rst_n)
begin
if(1'b0
== rst_n)
my_memory[0] <= 'd0;
else
if('d255 ==
my_memory[0])
my_memory[0] <= 'd0;
else
if(1'b1 ==
AddEn)
my_memory[0] <=
my_memory[0] + ADD_VAL;
end
always@(posedge sclk or
negedge rst_n)
begin
if(1'b0
== rst_n)
Memory0 <= 'd0;
else
Memory0 <= my_memory[0];
end
endmodule
|
2、Adder.v
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
|
|
`timescale 1ns/100ps
module Adder(
input
wire
sclk,
input
wire
rst_n,
input
wire
AddEn,
output
wire
[8-1:0]
AddResult
);
parameter WIDTH =
8;
parameter ADD_VAL =
2;
reg [WIDTH-1:0]
AddCnt;
wire [WIDTH-1:0]
Memory0;
wire [WIDTH-1:0]
Memory1;
always @(posedge sclk
or negedge rst_n)
begin
if(1'b0
== rst_n)
AddCnt <= 'd0;
else
if('d255 == AddCnt)
AddCnt <= 'd0;
else
if(1'b1 ==
AddEn)
AddCnt <= AddCnt + ADD_VAL;
end
//模块例化过程中,在模块调用过程中修改参数的方式 修改模块内常量值 Memory
#(2) Memory_inst(
.sclk
(sclk),
.rst_n (rst_n),
.AddEn (AddEn),
.Memory0(Memory0)
);
//模块例化过程中,使用defparam方式修改模块内常量值 defparam
Memory_inst1.ADD_VAL =
3;
Memory Memory_inst1(
.sclk
(sclk),
.rst_n (rst_n),
.AddEn (AddEn),
.Memory0(Memory1)
);
assign AddResult = AddCnt;
endmodule
|
3、tb_Adder.v
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
|
|
`timescale 1ns/100ps
module tb_Adder();
reg
sclk_i;
reg
rst_n_i;
reg
AddEn_i;
wire
[8-1:0]
AddResult_i;
initial begin
sclk_i =
0;
rst_n_i =
0;
AddEn_i =
0;
#100;
rst_n_i =
1;
#100;
AddEn_i =
1;
end
//20ns时钟周期 always #10
sclk_i = ~sclk_i;
//Adder:原始模块名; Adder_inst:例化后的模块名 Adder
Adder_inst(
.sclk
(sclk_i),
.rst_n
(rst_n_i),
.AddEn
(AddEn_i),
.AddResult(AddResult_i)
);
endmodule
|
4、run.do
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
|
quit -sim
vlib work
vlog ./../include/*.h
vlog ./../source/*.v
vlog ./tb_Adder.v
vsim -voptargs=+acc work.tb_Adder
add wave -color red
tb_Adder/Adder_inst/AddCnt
add wave tb_Adder/Adder_inst/*
run 1000ns
|
5、modelsim_run.bat
6、memory.dat
1
2
3
4
5
6
7
8
9
10
11
12
|
|
// Comments are allowed
CC
// This is first address
i.e AA
// This is second
address i.e @08
// Jump to new
address 8'h08 5A
// This is
address 8'h09 69
// This
is address 8'h0a 5A
// This
is address 8'h0b 69
// This
is address 8'h0c 5A
// This
is address 8'h0d 69
// This
is address 8'h0e 5A
// This
is address 8'h0f
|
7、仿真结果
