范围
本页是有关 FIFOs的系列五页中的第三页,展示了 baseline single clock FIFO在 Verilog 中的实现。这对于编写 portable 代码很有用,但本页的重点是重申 FIFO 的工作原理。因此,我将展示“标准 FIFO(standard FIFO)”和 FWFT FIFO的实现。但首先我们需要一 dual port RAM ,两 FIFOs 都会使用。
dual port RAM
这就是凭借 inference实现 RAM 的 Verilog 模块。很可能任何综合工具(synthesizer)都能做到这一点,但它可能会生成不希望的 RAM (block RAM 与 distributed RAM),因此可能有必要添加综合工具指令(synthesizer directives)。或者,如果效果最好,也可以使用 FPGA 供应商为 dual port RAM提供的 IP 。
所以这里是模块:
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 (注意 always @(*) 语句)计算为组合函数(combinatorial function)。 @next_words_in_ram 用于通过以下代码片段生成多个寄存器(registers)的值:
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 。
与 dual-clock FIFO相比, FIFO 如此易于实现的原因在于能够像这样定义 @words_in_ram ,并在 FIFO的两侧使用相同的寄存器(register)。
接下来在代码中,我们更新了 @rd_addr 和 @wr_addr,然后是 @rst的子句。请注意,复位(reset)会导致 @empty 和 @full 都变为高电平,但当复位释放时, @full 将返回低电平。
关于编码风格的说明: 当 @rst 为高时, if statement的 begin-end 子句中的赋值会覆盖之前可能进行的赋值,因此 @rst 确实将所有寄存器重置为其初始值。这不是最常见的编码风格,但当不是所有寄存器都被重置时,它具有明显的优势。这个具体案例并没有展示这种优势,但请参阅此页面。
最后,还有 dual port RAM的例化(instantiation)。
FWFT FIFO
如本页所示,将“标准 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;
回想一下, @empty 在 FWFT FIFO上意味着“@dout 无效”。所以这个赋值意味着除了 @rd_en之外,如果输出(output)无效,并且内存阵列中有数据可以读取,请继续执行此操作。那就是 first word的 falling through 。
最后,用于分配 @empty 的逻辑已更改为
always @(posedge clk)
if (fetch_data)
empty <= 0;
else if (rd_en)
empty <= 1;
这简单地说,如果从内存阵列中读取一个字, @empty 会在下一个时钟周期(clock cycle)上变为低电平,因为现在显然有新的有效数据。但是,如果这没有发生,并且 @rd_en 仍然启用,那么应用逻辑刚刚使用了最后一个可用的单词,因此将 @empty 更改为高电平。请注意,如果 @rd_en 为高而 @fetch_data 为低,则 @has_more_words 肯定为低(请参阅上面 @fetch_data 的分配)。这就是为什么这个条件相当于读取 FIFO中的最后一个字。
关于 FIFOs的本系列的第三页到此结束。下一页显示如何调整“标准 FIFO”以在数据采集(data acquisition)应用程序中使用。