本节内容对双口RAM进行深度测试与仿真,主要内容是对a,b端口不同时钟域的读写测试。将生成的双口RAM例化如下:
|
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 |
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
测试程序如下:
|
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
`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的数据帧使用时会详细讨论。
对应视频: