学习路线:
- 同步FIFO设计 (附源码RTL/TB)
- 异步FIFO设计 (附源码RTL/TB)
- uart通信协议 (附源码RTL/TB)
- SPI协议接口设计 (附源码RTL/TB)
- AMBA 3 APB 接口设计 (附源码RTL/TB)
- AMBA AHB 接口设计 (附源码RTL/TB)
- AMBA AXI 接口设计 (附源码RTL/TB)
- UART 2 APB bridge设计 (附源码RTL/TB)
- APB 2 UART bridge设计 (附源码RTL/TB)
1、基本概念
- FIFO是什么,有什么用?
- 同步FIFO的“同步”是什么意思?
- 接口都有什么:
FIFO 是一种先进先出的数据缓存器。FIFO和RAM可以说是一对孪生兄弟,先进先出,顾名思义,他不可以像RAM一样根据地址读写,只能顺序的写入和读出数据,其数据地址由内部读写指针自动加 1 完成,这是她和RAM最大的区别,也因此决定了他们的使用场景不同。从逻辑上讲,FIFO是有RAM和控制逻辑共同组成的。从接口时序上讲,少了读地址和写地址信号,多了空信号和满信号。其他配置,比如大小配置、工作频率等特性都是和RAM一致的。
同步FIFO,读和写应用同一个时钟。它的作用一般是做交互数据的一个缓冲,也就是说它的主要作用就是一个buffer。而buffer来实现速率匹配。
异步FIFO,读写应用不同的时钟,它有两个主要的作用,一个是实现数据在不同时钟域进行传递,另一个作用就是实现不同数据宽度的数据接口。
既然是数据缓冲器,那么缓冲器的大小,存储深度,读写地址和存储器空满状态都需要确定。
一般FIFO使用循环指针(计数溢出自动归零)。一般可以称写指针为头head,读指针为尾tail。初始化时,读写指针指向同一数据地址。
上图可见,FIFO初始化时,WP和RP指针指向同一数据单元。WP指向下一个将要写入的数据单元,RP指向将要读出的数据单元,两者是一个追赶过程。可以设置一个计数器,只写,来一个数据,写一个,写地址,+1,计数器+1,写满为止;只读,来一个,读出一个数据,读地址+1,计数器-1;同时读写,计数器值不变,读写地址均+1。
三、Spec
(1) Function description
同步FIFO实现了对write/read的控制,其接口解决了接口两端数据速率不匹配的问题。
(2) Feature list
- 支持存储宽度、深度可配置
- 时钟工作频率为1MHz
(3) Block diagram
模块主要分为读/写接口、读/写指针、读写指针的比较逻辑和array存储阵列四部分。
- 读/写接口:为模块提供读写数据和读写使能信号;
- 读写指针:主要标志读写指针当前array的地址
- 比较逻辑:
- 使用element counter(elem_cnt)记录FIFO RAM 中的数据个数:
▷ 等于0时,给出empty信号;等于BUF_LENGTH时,给出full信号 - elem_cnt:
▷ 写而未满时增加1
▷ 读而未空时减1
▷ 同时发生读写操作时,elem_cnt不变
(4) Interface description
(5) Timing
分为三部分,写操作,读操作,读写操作。
四、RTL design
- DUT模块
module sync_fifo
#(
parameter DATA_WIDTH = 32,
parameter DATA_DEPTH = 8 ,
parameter PTR_WIDTH = 3
//parameter PTR_WIDTH = $clog2(DATA_DEPTH)
)
(
input wire clk_i ,
input wire rst_n_i ,
//write interface
input wire wr_en_i ,
input wire [DATA_WIDTH-1:0] wr_data_i,
//read interface
input wire rd_en_i ,
output reg [DATA_WIDTH-1:0] rd_data_o,
//Flags_o
output reg full_o ,
output reg empty_o
);
reg [DATA_WIDTH-1:0] regs_array [DATA_DEPTH-1:0];
reg [PTR_WIDTH-1 :0] wr_ptr ;
reg [PTR_WIDTH-1 :0] rd_ptr ;
reg [PTR_WIDTH :0] elem_cnt ;
reg [PTR_WIDTH :0] elem_cnt_nxt ;
//Flags
wire full_comb ;
wire empty_comb ;
/*---------------------------------------------------\
--------------- write poiter addr ----------------
\---------------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
wr_ptr <= 3'b0;
end
else if (wr_en_i && !full_o) begin
wr_ptr <= wr_ptr + 3'b1;
end
end
/*---------------------------------------------------\
-------------- read poiter addr ------------------
\---------------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
rd_ptr <= 3'b0;
end
else if (rd_en_i && !empty_o) begin
rd_ptr <= rd_ptr + 3'b1;
end
end
/*---------------------------------------------------\
--------------- element counter ------------------
\---------------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
elem_cnt <= 4'b0;
end
else if (wr_en_i && rd_en_i && !full_o && !empty_o) begin
elem_cnt <= elem_cnt;
end
else if(wr_en_i && !full_o) begin
elem_cnt <= elem_cnt + 1'b1;
end
else if(rd_en_i && !empty_o) begin
elem_cnt <= elem_cnt - 1'b1;
end
end
/*---------------------------------------------------\
------------- generate the flags -----------------
\---------------------------------------------------*/
always @(*) begin
if(!rst_n_i) begin
elem_cnt_nxt = 1'b0;
end
else if(elem_cnt != 4'd0 && rd_en_i && !empty_o) begin
elem_cnt_nxt = elem_cnt - 1'b1;
end
else if(elem_cnt != 4'd8 && wr_en_i && !full_o) begin
elem_cnt_nxt = elem_cnt + 1'b1;
end
else begin
elem_cnt_nxt = elem_cnt;
end
end
assign full_comb = (elem_cnt_nxt == 4'd8);
assign empty_comb = (elem_cnt_nxt == 4'd0);
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
full_o <= 1'b0;
end
else begin
full_o <= full_comb;
end
end
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
empty_o <= 1'b1;
end
else begin
empty_o <= empty_comb;
end
end
/*---------------------------------------------------\
-------------------- read data -------------------
\---------------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
rd_data_o <= 32'b0;
end
else if(rd_en_i && !empty_o) begin
rd_data_o <= regs_array[rd_ptr];
end
end
/*---------------------------------------------------\
------------------- write data -------------------
\---------------------------------------------------*/
reg [PTR_WIDTH:0] i;
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
for(i=0;i<DATA_DEPTH;i=i+1) begin
regs_array[i] <= 32'b0;
end
end
else if(wr_en_i && !full_o) begin
regs_array[wr_ptr] <= wr_data_i;
end
end
endmodule
- tb
module tb_sync_fifo;
reg clk_i ;
reg rst_n_i ;
reg wr_en_i ;
reg [31:0] wr_data_i;
reg rd_en_i ;
reg [31:0] rd_data_o;
wire full_o ;
reg empty_o ;
initial begin
rst_n_i = 1 ;
clk_i = 0 ;
rd_en_i = 0 ;
wr_en_i = 0 ;
wr_data_i = 32'b0;
#2 rst_n_i = 0 ;
#5 rst_n_i = 1 ;
end
initial begin
#10 wr_en_i = 1;
rd_en_i = 0;
#10 wr_en_i = 0;
rd_en_i = 1;
#10 wr_en_i = 1;
rd_en_i = 0;
#3 rd_en_i = 1;
#10
repeat(100) begin
#5 wr_en_i = {$random}%2;
rd_en_i = {$random}%2;
end
end
initial #2000 $finish;
always #0.5 clk_i = ~clk_i ;
always #1 wr_data_i = {$random}%10;
sync_fifo u_sync_fifo
(
.clk_i (clk_i ),
.rst_n_i (rst_n_i ),
.wr_en_i (wr_en_i ),
.wr_data_i(wr_data_i),
.rd_en_i (rd_en_i ),
.rd_data_o(rd_data_o),
.full_o (full_o ),
.empty_o (empty_o )
);
initial begin
$fsdbDumpfile("sync_fifo.fsdb");
$fsdbDumpvars ;
$fsdbDumpMDA ;
end
endmodule
五、分析和小结
(1)分析
- 写阶段
复位之后,进行写操作,直至写满,产生满标志后,不再写入新数据。
- 读阶段
进行读操作,直至读空,产生空标志后,不再读出新数据。
- 同时读写阶段
先进行写操作,写入三个新数据之后,同时进行读写操作,期间写入新数据和读出数据,但是elem_cnt计数器不再变化,动态平衡。
(2)小结
设计思路:先分析需求,定义接口,画出具体的实现框图;按照协议和理解,画出相应时序图;看图写程序,验证仿真波形是否与时序图对应。
同步FIFO设计要点是什么时候产生空满标志位,即怎么衡量array被写满或者被读空。在这里,我使用了4bit的elem_cnt表示,通过elem_cnt的值表示当前array存储阵列的资源使用情况。0表示没有数据,即空状态;8表示写满,因为array的存储深度就是8。在spec中提到实现FIFO可配置,在这里只实现了宽度为32bit,深度为8的同步fifo设计,初步验证仿真波形与时序图相对应。
————————————————
版权声明:本文为CSDN博主「xlinxdu」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_43244515/article/details/124163224