这篇更多的是记录FIFO学习,参考了众多优秀的文章,我都会标记出来,首先是关于同步FIFO的参考搞懂FIFO书写的关键是什么,异步FIFO参加这篇异步FIFO讲解
FIFO 的英文全称是 First In First Out,即先进先出。 FPGA 使用的 FIFO 一般指的是对数据的存储具有先进先出特性的一个缓存器,常被用于数据的缓存,或者高速异步数据的交互也即所谓的跨时钟域信号传递。它与 FPGA 内部的 RAM 和 ROM 的区别是没有外部读写地址线,采取顺序写入数据,顺序读出数据的方式,使用起来简单方便,由此带来的缺点就是不能像 RAM 和 ROM 那样可以由地址线决定读取或写入某个指定的地址。
为了增强对FIFO的认识,用Verilog描述一个FIFO。
FIFO是基于双口RAM衍生出来的,同步FIFO和异步FIFO的区别是主要是双口RAM的两套独立端口是否使用同一时钟,同步FIFO读写受同一时钟控制,其中设计的同步FIFO的信号如下所示。
FIFO大致分为写端和读端,两者都拥有独自的使能信号,以及写数据,这里数据宽度是8Bit,输出端对于FIFO的空满有相应的标志位,以及计数器和读出的数据。
FIFO读写的规则:
FIFO在外部端口上表现没有地址线,因为是第一个到达的数据也将是第一个输出的数据,所以 FIFO缓冲区是一种读/写存储阵列,可自动跟踪数据进入模块的顺序并以相同顺序读出数据。而内部的实现需要两个指针来分别表示读和写的地址,读指针和写指针。初始时读写指针指向第一个存储单元,此时FIFO队列为空,fifo_empty有效,当写入256个数据,也就是写指针重新指向第一个FIFO的存储单元,而计数data_count等于FIFO的深度(0~255,但是写指针总是指向在下一个写的存储单元,所以当写完地址为255,8’b1111 1111后,变为9位的256 = 9‘b1 0000 0000)时,fifo_full有效。由此可知读写指针相同,此时有可能是满状态(写一直有效,写完最后一个255地址后,取低8位= 8’b0000 0000,此时读写指针的地址相同),所以用地址差不能用于判断空满,而是使用计数器来判断,但是计数器要设置比深度的位数多一位 ,这里也就是9位。
上面提到的博文中的这幅图,结合上一段的解释可以帮助理解地址的变化:
其实更加规范的模式是想其中的位宽设置为参数,接下来的代码书写使用参数声明宽度。
//FIFO两条铁律就是不能满写 不能空读
module custom_FIFO(
input wire clk,
input wire rst_n,
input wire wr_en,
input wire [7:0] wr_data,
input wire rd_en,
output wire fifo_full, //空和满都是1有效
output reg [7:0] rd_data,
output wire fifo_empty,
output reg [8:0] data_count
);
reg [7:0] mem [255:0];
reg [7:0] wr_add;
reg [7:0] rd_add;
//FIFO的书写方式应该是分信号进行书写
//wr_add总是指向下一个写的存储单元,在上升沿监测到写地址为255时,实际上的地址值重新因为溢出指向了地址0,此时溢出信号为真,而计数值达到256
//这里的逻辑是读写地址会自动从255变为0
always@(posedge clk)begin
if(rst_n == 1'b0)begin
wr_add <= 8'b0;
end
else if(wr_en == 1'b1 && fifo_full== 1'b0) begin
wr_add <= wr_add + 1'b1;
end
end
// rd_add
always@(posedge clk)begin
if(rst_n == 1'b0)begin
rd_add <= 8'b0;
end
else if(rd_en == 1'b1 && fifo_empty == 1'b0) begin
rd_add <= rd_add + 1'b1;
end
end
//rd_data
always@(posedge clk)begin
if(rst_n == 1'b0)begin
rd_data <= 8'h00;
end
else if(rd_en == 1'b1 && fifo_empty == 1'b0)begin
rd_data <= mem[rd_add];
end
end
//wr_data
always@(posedge clk)begin
if(wr_en == 1'b1 && fifo_full== 1'b0)begin
mem[wr_add] <= wr_data;
end
end
always@(posedge clk)begin
if(rst_n == 1'b0)begin
data_count <= 9'h0;
end
else begin
if(wr_en == 1'b1 && !rd_en && fifo_full== 1'b0)begin //当读写都有效,则不变
data_count <= data_count + 1'b1;
end
else if(rd_en == 1'b1 && wr_en && fifo_empty == 1'b0)begin
data_count <= data_count - 1'b1;
end
end
end
assign fifo_full = (data_count == 9'h100); //数据计数的范围是1-256,当为256时数据满
assign fifo_empty = (data_count == 9'h00);
endmodule
读取的TXT中有259个数据,可以看到第256个数据为8‘h25,仿真文件中将所有的259个数据全部读取到mem中,然后将读到的数据写256到FIFO中。
module tb_custom_FIFO();
reg clk;
reg rst_n;
reg wr_en;
reg [7:0] wr_data;
reg rd_en;
wire fifo_full; //空和满都是1有效
wire [7:0] rd_data;
wire fifo_empty;
wire [8:0] data_count;
reg [7:0] mem [258:0];
always begin
#5 clk = ~clk;
end
initial begin
clk = 0;
rst_n = 1'b1;
#10;
rst_n = 1'b0;
#10 rst_n = 1'b1;
end
initial begin
$readmemh("D:/vivado_pf/Basic_pratice/uart_data.txt",mem);
$display("the file is load to mem");
$display("Read memory1: %h", mem[0]) ;
$display("Read memory2: %h", mem[1]) ;
$display("Read memory3: %h", mem[2]) ;
$display("Read memory4: %h", mem[3]) ;
end
initial begin
wr_en <= 1'b0;
rd_en <= 1'b0;
wr_data <= 8'h00;
#50;
wr_data_to_fifo();
#2620;
rd_en <= 1'b1;
end
//写256个数据到FIFO中
task wr_data_to_fifo(); //任务没有返回值
integer i;
begin
for (i = 0; i < 257; i = i + 1)begin
wr_data = mem[i];
wr_en = 1;
#10; //任务不能出现always语句,但是可以包含时序控制,任务可以没有或者多个输入、输出
end
wr_en = 0;
end
endtask
custom_FIFO custom_FIFO_inst(
.clk(clk),
.rst_n(rst_n),
.wr_en(wr_en),
.wr_data(wr_data),
.rd_en(rd_en),
.fifo_full(fifo_full),
.rd_data(rd_data),
.fifo_empty(fifo_empty),
.data_count(data_count)
);
endmodule
写数据在上升沿前后要有效一段时间,首先是正常工作的仿真,在设置的时候就没有溢出的情况,第一张图是开始写入,第二张图是写入结束。
修改仿真文件,第一张表示溢出写,可以看到,溢出写无效。第二章可以看到读空出两个单元后,写入读写地址相同也就停止了写。到这里同步的FIFO功能就是正确的。
该系统函数可用于计算寻址给定大小的存储器所需的最小地址宽度或表示给定数量的状态所需的最小向量宽度。
//位宽计算函数
function integer clog2 (input integer depth);
begin
for (clog2=0; depth>0; clog2=clog2+1)
depth = depth >>1;
end
endfunction
//$clog2(8) == 3;
使用方法
parameter p_cnt_max = p_rev_time*p_clk_fre*1000_000 - 1; //翻转时间内所需计数的最大值
wire [$clog2(p_cnt_max)-1:0] w_cnt_max;
还是参考博主的讲解,学习这个知识
FPGA中FIFO的实现可以使用分布式资源或者BLOCK RAM,当使用FIFO缓冲空间较小时,我们选择使用Distributed RAM;当使用FIFO缓冲空间较大时,我们选择使用BLOCK RAM资源;这是一般的选择原则。而我们在声明我们的mem类型的变量时,通过添加约束(*ram_style = “distributed”)/(*ram_style = “block”)来指定。
(*ram_style = "distributed"*) reg [DATA_WIDTH - 1 : 0] fifo_buffer[0 : DATA_DEPTH - 1];
或者:
(*ram_style = "block"*) reg [DATA_WIDTH - 1 : 0] fifo_buffer[0 : DATA_DEPTH - 1];
————————————————
版权声明:本文为CSDN博主「李锐博恩」的原创文章
原文链接:https://blog.csdn.net/Reborn_Lee/article/details/106619999
这个点我想补充一下,在底层是如何实现的,VIVADO中资源之间的关系,FPGA 主要资源为: CLB、DSP、Block RAM、CMTs、GT以及XADC等等。一个CLB由2个Slice组成,Slice分为SLICEM和SLICEL,一个CLB里最多有一个SLICEM,即一个CLB可由两个SLICEL或一个SLICEL加一个SLICEM组成。SLICEL可用于逻辑,算术运算, SLICEM除了用于逻辑,算术运算外,还可配置成分布式RAM或32位的移位寄存器。
也就是上面同步FIFO中的方式,优点是:逻辑简单,便于理解,且能够获得当前FIFO中的数据量;缺点:资源使用多,代码量大
增加的一位用来表示轮数,如果两个的附加位相同说明处于同一轮,如果附加位不同,则处于不同的轮数,此时,对于深度为2n的FIFO,需要的读/写指针位宽为(n+1)位,如对于深度为8的FIFO,需要采用4bit的计数器,0000~1000、1001~1111,MSB作为折回标志位,而低3位作为地址指针。
判空满的逻辑如下图展示更加清晰:
自然二进制数在表示一个连续变化的数值时,可能会有多个位同时发生变化,每个位翻转(变化)的频率是比较高的,这在某些应用场合,如在FPGA内部跨时钟域传输数据时,是十分不利的。
上图展示了格雷码,十进制0-15的格雷码的对应关系,格雷码有两个特性,一个是循环特性,一个是单布特性。
不过格雷码也有一个缺点,那就是相比于自然二进制码来说,它是一种无权码(而自然二进制码实际上是“8421”码,因此很难直接进行比较和数学运算,所以一般都需要将采集到的以格雷码为表示形式的数据先转换成自然二进制码,然后再参与运算。
异步FIFO通过对比读写指针来进行满空判断,但是读写地址属于不同的时钟域,在比较之前需要将读写地址进行同步处理,然后进行判空满操作后才能够读写。这样会存在两个问题:第一个是跨时钟域容易发生亚稳态,导致判断出错。第二个问题是时钟是不同频,或者读快写慢,或者读慢写快,这时候尽管进行了地址同步,也可能有一定的滞后性。以读慢写快为例,在写入端需要判满,因为写的时钟快,所以对于同步后的读端的地址不会遗漏,读的地址只存在等于小于当前读地址,所以满标志只会提前产生。在读端,同步写的地址,写时钟满,必然会遗漏一部分地址,且及有可能写地址小于真实地址,所以,空标志也会提前产生,尽管地址有遗漏,但时对于FIFO的逻辑操作不会产生影响。这里不管是采用二进制编码还是格雷码编码,都会存在同步后的读写地址不符合实际情况,但是依然能够保证FIFO功能的正确性,需要注意gray码只是在相邻两次跳变之间才会出现只有1位数据不一致的情形,超过两个周期则不一定,所有地址总线bus 偏差一定不能超过一个周期,否则可能出现gray码多位数据跳变的情况,这个时候gray码就失去了作用,因为这时候同步后的地址已经不能保证只有1位跳变了。
可以看到二进制编码的地址跨时钟同步最主要的问题是要消除亚稳态以及减少跳变。binary编码的地址总线在跳变时极易产生毛刺,因为binary编码是多位跳变,在实现电路时不可能做到所有的地址总线等长,address bus 偏差必然存在,而且写地址和读地址分属不同时钟域异步,这样地址总线在进行同步过程中出错不可避免,比如写地址在从0111到1000转换时4条地址线同时跳变,这样读时钟在进行写地址同步后得到的写地址可能是0000-1111的某个值,这个完全不能确定,所以用这个同步后的写地址进行FIFO空判断的时候难免出错。
通过上面的分析可得,异步FIFO采用地址增加额外一位来判满,然后通过将二进制地址转换位格雷码,降低读写地址在连续变换时的位翻转概率,跨时钟域,然后大拍消除亚稳态,然后在转换为格雷码,进行地址比较,判断满空状态。
格雷码解决了一个问题,但同时也带来了另一个问题,即格雷码如何判断空与满?
**对于“空”的判断依然是两者完全相等(**包括增加的位);
对于“满”的判断由于于gray码除了MSB外,具有镜像对称的特点,存储深度为8,最多MSB表示的读写之间之间最大的差距是一轮,所以,格雷码的表示的深度是16。例如,当读指针指向7,写指针指向8时,除了MSB,其余位皆相同,但这个判满的条件显然不成立。因此不能单纯的只检测最高位了,在gray码上判断为满必须同时满足以下3条:
1.wptr和同步过来的rptr的MSB不相等,因为wptr必须比rptr多折回一次。
2.wptr与rptr的次高位不相等,如上图位置7和位置15,转化为二进制对应的是0111和1111,MSB不同说明多折回一次,111相同代表同一位置。
3.剩下的其余位完全相等。
该过程也称为格雷码的编码,方法是从二进制码的最右边一位(最低位)起,依次将每一位与左边一位进行异或运算,作为对应格雷码该位的值,而最左边一位(最高位)不变。 对应公式如下:
g[n] <= b[n] //其中g[n]代表的是n位的格雷码,b[n]代表的是二进制码
g[n] <= g[n] ^ (g[n}] >> 1)
例如,将自然二进制码“10110”转换为格雷码,可以形象的用下图表示其转换过程:
该过程也称为格雷码的解码,方法是从格雷码左边第二位(次高位)起,将每一位与其左边一位解码后的值异或,作为该位解码后的值,而最左边一位(最高位)的解码结果就是它本身。对应公式如下:
b[n] <= g[n] //其中g[n]代表的是n位的格雷码,b[n]代表的是二进制码
b[n] <= g[n]^b[n+1] //只需要计算n-1位 的即可
例如,将格雷码“11101”转换为自然二进制码,可以形象的用下图表示其转换过程:
代码实现的过程要注意当二进制的最高位和格雷码的最高位是相同的,不需要计算直接赋值。
module gray2bin # (
parameter N = 4;
)(
input [N-1:0] gray,
output [N-1:0] bin
);
assign bin[N-1] = gray[N-1];
genvar i;
generate
for(i = N-2; i >= 0 ;i++)begin:gray_2_bin
bin[i] <= gray[i] ^ bin[i+1];
end
endgenerate
endmodule
图和代码是对应的,以左边的写端为例输入到fifo_mem的地址为waddr[ADDR_SIZE-1 : 0],waddr[ADDR_SIZE:0] 转换格雷码为waddr_gray,在读端打两拍分为为:wr_gray2,wr_gray2。
`timescale 1ns / 1ps
module asy_FIFO_gray#(
parameter DATA_WIDTH = 8, //数据位宽
parameter DATA_DEPTH = 8 //数据深度
)(
input wire wclk,
input wire wrst_n,
input wire winc, //有效时写入数据
input wire [DATA_WIDTH-1 : 0] wdata,
output wire wfull,
input wire rclk,
input wire rrst_n,
input wire rinc,
output reg [DATA_WIDTH-1 : 0] rdata,
output wire rempty
);
localparam ADDR_SIZE = $clog2(DATA_DEPTH); //ARR_SIZE = 3;
reg [ADDR_SIZE : 0] waddr,raddr; //注意这里的地址宽度[3:0],是5
wire [ADDR_SIZE : 0] waddr_gray, raddr_gray; //转换后的格雷码位宽为[5:0]
reg [ADDR_SIZE : 0] wr_gray1, wr_gray2; //读时钟域———打拍后的格雷码
reg [ADDR_SIZE : 0] rw_gray1, rw_gray2; //写时钟域———打拍后的格雷码
//二进制转换为格雷码
assign waddr_gray = waddr ^ (waddr >> 1) ; //逻辑移位与算术移位的右移符号分别为“>>”和“>>>”,左移同理,逻辑移位不考虑符号位
assign raddr_gray = raddr ^ (raddr >> 1) ; //左移和右移都只补零;算术移位考虑符号位,左移补零,右移补符号位
//将读端的读格雷码地址同步到写端
//主要功能是将读写端的格雷码进行打拍,r_to_w
//首先要注意格雷码的位宽大于地址位宽一位,所以rptr位宽为6,addrsize = 5,[5;0]
//1
always @(posedge wclk or negedge wrst_n)begin
if (!wrst_n)
{rw_gray2,rw_gray1} <= 0;
else
{rw_gray2,rw_gray1} <= {rw_gray1,raddr_gray};
end
//将写端的写格雷码地址同步到读端
//主要功能是将读写端的格雷码进行打拍,w_to_r
//2
always @(posedge rclk or negedge rrst_n)begin
if (!rrst_n)
{wr_gray2,wr_gray1} <= 0;
else
{wr_gray2,wr_gray1} <= {wr_gray1,waddr_gray};
end
//存储器读写,写信号的有效需要判断你,读总是有效的,读出当前地址内的数据
reg [DATA_DEPTH-1 : 0] fifo_mem [DATA_WIDTH-1 : 0];
wire wclken, rclken;
//写端,当写有效时,写入数据
always@(posedge wclk or negedge wrst_n) begin
if(wrst_n == 0) begin
waddr <= 0;
end
else if(wclken) begin
fifo_mem[waddr[ADDR_SIZE-1 : 0]] <= wdata;
waddr <= waddr + 1;
end
end
//读端,当读有效时,读出数据
always@(posedge rclk or negedge rrst_n) begin
if(rrst_n == 0) begin
raddr <= 0;
end
else if(rclken) begin
rdata <= fifo_mem[raddr[ADDR_SIZE-1 : 0]] ;
raddr <= raddr + 1;
end
end
//判满逻辑
//wire wfull;
localparam ADDRSIZE = $clog2(DATA_DEPTH); //ARR_SIZE = 3;
assign wfull = wrst_n? (waddr_gray=={~rw_gray2[ADDRSIZE:ADDRSIZE-1], rw_gray2[ADDRSIZE-2:0]}) : 1'b0;
//判空逻辑
//wire rempty;
assign rempty = rrst_n ? (raddr_gray == wr_gray2): 1'b0;
assign wclken = winc & !wfull;
assign rclken = rinc & !rempty;
endmodule
其中需要注意的有如下几点:
`timescale 1ns / 1ps
module sim_asy_FIFO_gray();
parameter DATA_WIDTH = 8;
parameter DATA_DEPTH = 8;
reg wclk;
reg wrst_n;
reg winc; //有效时写入数据
reg [DATA_WIDTH-1 : 0] wdata;
wire wfull;
reg rclk;
reg rrst_n;
reg rinc;
wire [DATA_WIDTH-1 : 0] rdata;
wire rempty;
initial begin
wclk = 0;
forever begin
#5 wclk = ~wclk;
end
end
initial begin
rclk = 0;
forever begin
#10 rclk = ~rclk;
end
end
initial begin
wrst_n = 1;
rrst_n = 1;
winc = 0;
rinc = 0;
#30
wrst_n = 0;
rrst_n = 0;
#30
wrst_n = 1;
rrst_n = 1;
//write data into fifo buffer
@(negedge wclk)
wdata = $random;
winc = 1;
repeat(8) begin
@(negedge wclk)
wdata = $random; // write into fifo 8 datas in all;
end
// read parts
@(negedge wclk)
winc = 0;
@(negedge rclk)
rinc = 1;
repeat(8) begin
@(negedge rclk); // read empty
end
@(negedge rclk)
rinc = 0;
//write full
# 80
@(negedge wclk)
winc = 1;
wdata = $random;
repeat(15) begin
@(negedge wclk)
wdata = $random;
end
@(negedge rclk)
rinc = 1;
repeat(8) begin
@(negedge rclk); // read empty
end
@(negedge rclk)
rinc = 0;
@(negedge wclk)
winc = 0;
#50 $finish;
end
asy_FIFO_gray #(
.DATA_WIDTH(DATA_WIDTH),
.DATA_DEPTH(DATA_DEPTH)
) inst_asy_FIFO_gray (
.wclk(wclk),
.wrst_n(wrst_n),
.winc(winc),
.wdata(wdata),
.wfull(wfull),
.rclk(rclk),
.rrst_n(rrst_n),
.rinc(rinc),
.rdata(rdata),
.rempty(rempty)
);
endmodule
这篇从原理和实现上都进行了记录,可以看到功能实现正确。如果有问题,可以评论交流。