本节内容对双口RAM进行深度测试与仿真,主要内容是对a,b端口不同时钟域的读写测试。将生成的双口RAM例化如下:
module dpram
(
//==================================a side
input inclka,
input [8:0] address_a,
input [15:0] data_a,
input wren_a,
input rden_a,
output [15:0] q_a,
//==================================b side
input inclkb,
input [8:0] address_b,
input [15:0] data_b,
input wren_b,
input rden_b,
output [15:0] q_b
);
d_p_ram d_p_ram_inst
(
.clock_a (inclka),
.address_a (address_a),
.data_a (data_a), //data --write to memory
.rden_a (rden_a),
.wren_a (wren_a),
.q_a (q_a), //read, data out from memory
.clock_b (inclkb),
.address_b (address_b),
.data_b (data_b),
.rden_b (rden_b),
.wren_b (wren_b),
.q_b (q_b)
);
endmodule
新生成的工程中双口RAM的示意图如图1

图1
1.双口RAM测试内容
为了能比较容易观察写入和读出数据,本测试设置写入的数据为16-bit 宽,为了观察a,b端口数据的输入与输出以及数据的综合应用,设计a,b端口对数据包的操作,设计如下:
- 异步时钟下,输出端口非寄存输出的情况下,观察a端口写入,a端口读出延时。a端口写入,b端口读出的延时。注:读出时选择读出旧数据(old data),如图2,
- 在状态1(st == 1)时,在地址为16’h0001的单元写入16’h55aa;

图2
测试程序如下:
`timescale 1ns /1ps
module addab
();
parameter PERIOD = 10;
reg inclka;
reg inclkb;
reg rst;
reg [8:0] address_a;
reg [15:0] data_a;
wire [15:0] q_a;
reg rden_a;
reg wren_a;
reg [8:0] address_b;
reg [15:0] data_b;
wire [15:0] q_b;
reg rden_b;
reg wren_b;
reg [15:0] q_b_r;
reg [2:0] st;
//assign inclkb=inclka;
initial
begin
rst = 1'b1;
inclka = 0;
inclkb = 0;
#50
rst = 0;
#500;
$stop;
end
always #(PERIOD/2) inclka = ~inclka;
always #(PERIOD/3) inclkb = ~inclkb;
always@(posedge inclka or posedge rst)
if(rst)
begin
st <= 0;
address_a <= 0;
rden_a <= 1'b0;
wren_a <= 1'b0;
data_a <= 0;
end
else
begin
case(st)
0:
begin
address_a <= 0;
rden_a <= 1'b1;
wren_a <= 1'b0;
data_a <= 0;
st <= 1;
end
1:
begin
address_a <= 1;
wren_a <= 1'b1;
data_a <= 16'h55aa;
st <= 2;
end
2:
begin
wren_a <= 1'b0;
st <= 3;
end
3:
begin
wren_a <= 1'b0;
st <= 4;
end
4:
begin
wren_a <= 1'b0;
st <= 5;
end
5:
begin
wren_a <= 1'b0;
st <= 6;
end
6:
begin
wren_a <= 1'b0;
st <= 6;
end
default: st <= 0;
endcase
end
always@(posedge inclkb or posedge rst)
if(rst)
begin
address_b <= 0;
rden_b <= 1'b0;
wren_b <= 1'b0;
data_b <= 0;
q_b_r <= 0;
end
else
begin
address_b <= 1;
rden_b <= 1'b1;
q_b_r <= q_b;
end
dpram dpram_inst
(
.inclka (inclka),
.address_a (address_a),
.data_a (data_a), //data --write to memory
.rden_a (rden_a),
.wren_a (wren_a),
.q_a (q_a), //read, data out from memory
.inclkb (inclkb),
.address_b (address_b),
.data_b (data_b),
.rden_b (rden_b),
.wren_b (wren_b),
.q_b (q_b)
);
endmodule
仿真波形如图3:

图3
- 波形分析:从图3可以看出,在状态机st为1时对a端口进行赋值,在状态机状态2(st == 2)时,地址address_a,数据data_a,写使能wren_a都得到相应的数据,但双口RAM内部对地址address_a,数据data_a,写使能wren_a都进行了锁存,因此写入到RAM中将会再延迟一个时钟,所以真正将数据写入到RAM中是在状态2(st == 2)结束时的时钟上升沿时刻,最终将数据写到RAM并且数据稳定是在状态3(st == 3)的状态,但是由于在生成双口RAM时选择了读取旧数据(old data),因此在状态3(st == 3)并没有及时读出,而实际读出数据是在状态4(st == 4)。但b端口由于时钟沿超前a端口,因此可以看到在状态3的中间位置,q_b已经输出在端口位置了,需要提醒的是,由于a,b 端口采用的是异步时钟,因此b端的数据可能不稳定。后面我们会针对这种情况提出专门的解决方案。
- 修改读出数据为新数据( new data),如图4,再观察仿真结果。

图4
测试条件不变,仿真波形如图5

图5
从图5的波形可以看出,由于在读出端口设置了读新数据,因此a端口读出的值比图3提前了一个时钟,但b端口输出并没有因为改成新数据提前一个时钟。注意在输入、输出为异步时钟时,读取新数据并不稳定,在使用新数据读取时一定要注意异步时钟的亚稳态效应。
- 异步时钟,输出端口寄存的情况下,观察a端口写入,a端口读出延时。a端口写入,b端口读出的延时,此时采用读取新数据(new data),如图6,

图6
测试条件不变,仿真结果如图7,

图7
从图7中可以看出,尽管读取新数据,但由于数据的数据采用寄存器寄存,因此又延时了一个时钟,a端口读取数据又推迟到了状态4。b端口读取数据推迟一个inclkb的时钟宽度。
2. 异步双端口使用建议
异步时钟由于在读写时存在数据读取不稳定的因素,因此在使用异步时钟时有如下建议:
- 尽量避免读写同时访问同一单元
- 输出建议使用寄存器寄存的方式可以避免亚稳态,如图6,
- 在无法避免读写同时访问同一单元时,建议设置读出为旧数据(old data)
- 可以设置标志位,判断标志位,内容延迟一个时钟读取,这个内容在双口RAM的数据帧使用时会详细讨论。
对应视频: