范围
本页是关于 FIFO 的五页系列教程中的第三页,演示如何在 Verilog 中实现一个基线单时钟 FIFO。这对于编写可移植代码可能很有用,但本页的重点是再次强调 FIFO 的工作原理。因此,我会给出“标准 FIFO”和 FWFT FIFO 的实现代码。不过,我们先需要一个双端口 RAM(dual-port RAM),因为两种 FIFO 都要用到它。
双端口 RAM
这个 Verilog 模块利用推断(inference)机制来实现一个双端口 RAM。任何综合器(synthesizer)都很可能能正确处理这种写法,但有时它生成的可能不是你想要的那种 RAM(块 RAM(block RAM)或分布式 RAM(distributed RAM)),所以可能需要添加综合器指令(synthesizer directive)。如果使用 FPGA 厂商提供的双端口 RAM IP 核(IP core)效果更好,也可以选择那条路。
下面就是这个模块:
module dualport_ram #(parameter depth = 64,
log2_depth = 6,
width = 8
)
(
input clk,
input [(log2_depth-1):0] wr_addr,
input [(log2_depth-1):0] rd_addr,
output reg [(width-1):0] rd_data,
input [(width-1):0] wr_data,
input rd_en,
input wr_en
);
reg [(width-1):0] inferred_ram[0:(depth-1)];
always @(posedge clk)
begin
if (wr_en)
inferred_ram[wr_addr] <= wr_data;
if (rd_en)
rd_data <= inferred_ram[rd_addr];
end
endmodule
“标准 FIFO”
下面可以来看实现“标准 FIFO”(也就是不是 FWFT FIFO)的模块:
module fifo
#(parameter depth = 64, // Must equal 2^log2_depth exactly
log2_depth = 6,
width = 8
)
(
input clk,
input rst,
input wr_en,
input [(width-1):0] din,
input rd_en,
output [(width-1):0] dout,
output reg full,
output reg empty
);
reg [log2_depth:0] next_words_in_ram; // Combinatorial
reg [log2_depth:0] words_in_ram;
reg [(log2_depth-1):0] rd_addr;
reg [(log2_depth-1):0] wr_addr;
wire fetch_data, commit_data;
assign fetch_data = rd_en && !empty;
assign commit_data = wr_en && !full;
always @(*)
if (commit_data && !fetch_data)
next_words_in_ram <= words_in_ram + 1;
else if (!commit_data && fetch_data)
next_words_in_ram <= words_in_ram - 1;
else
next_words_in_ram <= words_in_ram;
always @(posedge clk)
begin
words_in_ram <= next_words_in_ram;
full <= (next_words_in_ram == depth);
empty <= (next_words_in_ram == 0);
if (fetch_data)
rd_addr <= rd_addr + 1;
if (commit_data)
wr_addr <= wr_addr + 1;
if (rst)
begin
empty <= 1;
full <= 1;
words_in_ram <= 0;
rd_addr <= 0;
wr_addr <= 0;
end
end
dualport_ram
#(.depth(depth), .log2_depth(log2_depth), .width(width)) dp_ins
(.clk(clk), .wr_addr(wr_addr),
.rd_addr(rd_addr),
.wr_en(commit_data),
.rd_en(fetch_data),
.wr_data(din),
.rd_data(dout)
);
endmodule
唯一重要的使用说明写在了代码顶部的注释里,与 depth 和 log2_depth 参数有关:depth 必须精确等于 2log2_depth。
这个模块的工作原理相当简单:@fetch_data 与 @rd_en 类似,但它把 @empty(空)也考虑进去了。因此 @fetch_data 可以算是 @rd_en 的安全版本——即使 @rd_en 和 @empty(空)在同一个时钟周期内同时为高(这是不允许的),也不会发生什么坏事,因为在这种情况下 @rd_en 会被忽略。
@commit_data 也是 @wr_en 的安全版本:它把 @full(满)考虑进去了。
@words_in_ram 的下一个值 @next_words_in_ram 是根据 @fetch_data 和 @commit_data 计算出来的组合逻辑函数(combinatorial logic function)——注意这里用的是 always @(*) 语句。@next_words_in_ram 被用来产生好几个寄存器的值,代码如下:
always @(posedge clk)
begin
words_in_ram <= next_words_in_ram;
full <= (next_words_in_ram == depth);
empty <= (next_words_in_ram == 0);
[ ... ]
请注意,@full(满)和 @empty(空)就是在这里定义的。
与双时钟 FIFO 相比,这个单时钟 FIFO 之所以如此容易实现,是因为我们可以这样定义 @words_in_ram,并在 FIFO 的两侧都使用这同一个寄存器。
再往下看代码,是对 @rd_addr 和 @wr_addr 的更新,然后是 @rst 所对应的 if 分支。请注意,@empty(空)和 @full(满)都会因为复位而变为高电平,但 @full(满)会在复位释放后变回低电平。
关于编码风格的一点说明:当 @rst 为高时,if 语句的 begin-end 块中的赋值会覆盖前面可能已经发生的赋值,所以 @rst 确实会把所有寄存器复位到它们的初始值。这不是最常见的编码风格,但在并非所有寄存器都需要复位的情况下,它有明显的优势。这里的具体例子没能体现出这一优势,不过可以参见这一页。
最后,代码中有一个对双端口 RAM 的实例化(instantiation)。
FWFT FIFO
正如这个页面所示,把“标准 FIFO”转换成 FWFT FIFO 相当容易。不过,直接实现 FWFT FIFO 可以让我们讨论一些有意思的细节,所以下面是它的实现代码:
module fwft_fifo
#(parameter depth = 64, // Must equal 2^log2_depth exactly
log2_depth = 6,
width = 8
)
(
input clk,
input rst,
input wr_en,
input [(width-1):0] din,
input rd_en,
output [(width-1):0] dout,
output reg full,
output reg empty
);
reg [log2_depth:0] next_words_in_ram; // Combinatorial
reg [log2_depth:0] words_in_ram;
reg [(log2_depth-1):0] rd_addr;
reg [(log2_depth-1):0] wr_addr;
reg has_more_words;
wire fetch_data, commit_data;
assign fetch_data = (rd_en || empty) && has_more_words;
assign commit_data = wr_en && !full;
always @(*)
if (commit_data && !fetch_data)
next_words_in_ram <= words_in_ram + 1;
else if (!commit_data && fetch_data)
next_words_in_ram <= words_in_ram - 1;
else
next_words_in_ram <= words_in_ram;
always @(posedge clk)
begin
words_in_ram <= next_words_in_ram;
full <= (next_words_in_ram == depth);
has_more_words <= (next_words_in_ram != 0);
if (fetch_data)
rd_addr <= rd_addr + 1;
if (commit_data)
wr_addr <= wr_addr + 1;
if (fetch_data)
empty <= 0;
else if (rd_en)
empty <= 1;
if (rst)
begin
empty <= 1;
full <= 1;
words_in_ram <= 0;
has_more_words <= 0;
rd_addr <= 0;
wr_addr <= 0;
end
end
dualport_ram
#(.depth(depth), .log2_depth(log2_depth), .width(width)) dp_ins
(.clk(clk), .wr_addr(wr_addr),
.rd_addr(rd_addr),
.wr_en(commit_data),
.rd_en(fetch_data),
.wr_data(din),
.rd_data(dout)
);
endmodule
首先注意,这里新增了一个寄存器 @has_more_words。把它的定义与上面“标准 FIFO”中的定义比较一下,你就会明白 @has_more_words 就是 @empty(空)的逻辑非(logical NOT)。
接下来注意,@fetch_data 的定义变了。现在改由 @has_more_words 来把关(这并不奇怪),如下所示:
assign fetch_data = (rd_en || empty) && has_more_words;
回想一下前面介绍的:在 FWFT FIFO 上,@empty(空)表示“@dout 无效”。所以这个赋值语句的意思是,除了 @rd_en 为高的情况之外,如果当前输出无效,而存储阵列中还有数据可读,那就照样去读。这正是第一个数据字“直通”出来的机制。
最后,对 @empty(空)的赋值逻辑变成了下面这样:
always @(posedge clk)
if (fetch_data)
empty <= 0;
else if (rd_en)
empty <= 1;
这段代码的意思很简单:如果从存储阵列中读出了一个数据字,那么 @empty(空)会在下一个时钟周期变为低电平,因为显然此时已经有了新的有效数据。但如果上述情况没有发生,而 @rd_en 仍然有效,那就说明应用逻辑刚刚取走了最后一个可用的数据字,所以要把 @empty(空)置为高。注意,如果 @rd_en 为高而 @fetch_data 为低,那么 @has_more_words 必定为低(参见上面 @fetch_data 的赋值)。这正是为什么这个条件等价于“读出了 FIFO 中的最后一个数据字”。
至此,本系列关于 FIFO 的第三页就结束了。下一页将展示如何把“标准 FIFO”改造用于数据采集(data acquisition)应用中。