同步FIFO的实现
首先是同步的实现,只需要一个时钟用来控制读写。
同步 FIFO 实现较为直接,如下图所示,一个 FIFO 内部实现了 RAM 和一个控制读写的控制端,和普通的随机存储器不一样的是,FIFO 内部需要自动产生读写地址。
控制端产生空、满信号。因此如何实现控制端去产生空满信号是主要需要考虑的地方。通常空满的判断是通过读、写地址进行的。
第一种思路,利用读写地址的大小来判断空、满
方法很简单,设置一个信号量 factor 来表识空满,每次读一个数据则令 factor - 1,写一个数据则令其+1。实现代码如下:
module fifo #(
parameter DATA_WIDTH=16,
parameter ADDR_WIDTH=8
)(
input clk ,
input rst_n ,
input write_en ,
input [DATA_WIDTH-1:0] write_data ,
input read_en ,
output reg [DATA_WIDTH-1:0] read_data ,
output reg empty ,
output reg full ,
output [ADDR_WIDTH-1:0] counter
);
reg [ADDR_WIDTH-1:0] read_addr;
reg [ADDR_WIDTH-1:0] write_addr;
reg [ADDR_WIDTH-1:0] factor;
assign counter = factor;
wire write_allow = (write_en && !full);
wire read_allow = (read_en && !empty);
// 实例化 RAM
dual_ram ram(clk, clk, rst_n, read_allow, write_allow, read_addr, write_addr, write_data, read_data);
[email protected](posedge clk or negedge rst_n) begin
if (!rst_n) begin
factor <= 'h0;
read_addr <= 'h0;
write_addr <= 'h0;
end
else if (write_allow) begin
factor <= factor + 1;
write_addr <= write_addr + 1;
end
else if (read_allow) begin
factor <= factor - 1;
read_addr <= read_addr + 1;
end
end
[email protected](posedge clk or negedge rst_n) begin
if (!rst_n) begin
empty <= 1;
end
else begin
empty <= ( !write_en && (factor[7:1] == 7'b0) && (factor[0] == 0 || read_en) );
//empty <= factor == 8'b0;
end
end
[email protected](posedge clk or negedge rst_n) begin
if (!rst_n) begin
full <= 0;
end
else begin
full <= ( !read_en && (factor[7:1] == 7'b1111111) && (factor[0] == 1 || write_en) );
//full <= (factor == 8'hFF);
end
end
endmodule
// 定义一个双端口 RAM
module dual_ram#(
parameter DATA_WIDTH=16,
parameter ADDR_WIDTH=8
)(
input read_clk,
input write_clk,
input rst_n,
input read_en,
input write_en,
input [ADDR_WIDTH-1:0] read_addr,
input [ADDR_WIDTH-1:0] write_addr,
input [DATA_WIDTH-1:0] write_data,
output reg [DATA_WIDTH-1:0] read_data
);
reg [DATA_WIDTH-1:0] mem [255:0];
integer i;
[email protected](posedge write_clk or negedge rst_n) begin: write_ram
if (!rst_n) begin
for (i = 0; i < 256; i = i + 1)
mem[i] <= {DATA_WIDTH{0}};
end
else if (write_en) begin
mem[write_addr] <= write_data;
end
end
[email protected](posedge read_clk or negedge rst_n) begin: read_ram
if (!rst_n) begin
read_data <= {DATA_WIDTH{0}};
end
else if (read_en) begin
read_data <= mem[read_addr];
end
end
endmodule
值得注意的是,上面的代码对空满的判断并不简简单单是判断 factor 的大小为 0 还是等于 FIFO 大小。例如对空信号的判断:
这句代码的意思就是,要想为空,那么写信号一定无效,同时如果如果 factor==0,那么肯定是空;然而还有一种极端情况,即在FIFO 内部只有一个数据时,读信号还有效,那么下个 cycle,FIFO 一定为空。
这样的判断有些繁琐,还需要两个 full、empty 寄存器。
第二种思路,采用读写地址位进行空、满判断
假设,实例化的 RAM 地址位为 4,那么我们可以定义 5 bit 的读写地址寄存器(read_addr_e、write_addr_e),高位用来当作标志位,用来标识读写的次数关系。但是在访问 RAM 时还是需要输入 4 bit 的地址。判断规则如下:
- i f w r i t e _ a d d r _ e = = r e a d _ a d d r _ e t h e n e m p t y = 1 if \quad write\_addr\_e == read\_addr\_e \quad then \quad empty=1 ifwrite_addr_e==read_addr_ethenempty=1(因为读写地址相同,意味着读指针、写指针重合)
- i f ( r e a d _ a d d r _ e [ 4 ] = w r i t e a d d r e [ 4 ] ) & & ( r e a d _ a d d r _ e [ 3 : 0 ] = = w r i t e _ a d d r _ e [ 3 : 0 ] ) t h e n f u l l = 1 if \quad (read\_addr\_e[4] \!= write_addr_e[4]) \&\& (read\_addr\_e[3:0] == write\_addr\_e[3:0]) \quad then \quad full=1 if(read_addr_e[4]=writeaddre[4])&&(read_addr_e[3:0]==write_addr_e[3:0])thenfull=1(因为标志位不同意味着写指针可能已经领先一圈,例如 00000,10000 表示已经写了 8 个数了,而还没有读出一个数,则满了)
实现代码如下:
module fifo #(
parameter DATA_WIDTH=16,
parameter ADDR_WIDTH=4
)(
input clk ,
input rst_n ,
input write_en ,
input [DATA_WIDTH-1:0] write_data ,
input read_en ,
output reg [DATA_WIDTH-1:0] read_data ,
output empty ,
output full
);
wire [ADDR_WIDTH-1:0] read_addr;
wire [ADDR_WIDTH-1:0] write_addr;
reg [ADDR_WIDTH :0] read_addr_e;
reg [ADDR_WIDTH :0] write_addr_e;
assign read_addr = read_addr_e[ADDR_WIDTH-1:0];
assign write_addr = write_addr_e[ADDR_WIDTH-1:0];
wire write_allow = (write_en && !full);
wire read_allow = (read_en && !empty);
// 为了更加简洁,直接定义了一个 memory
reg [DATA_WIDTH-1:0] mem [15:0];
integer i;
[email protected](posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (i = 0; i < 8; i = i + 1)
mem[i] <= 'b0;
end
else if (write_allow)
mem[write_addr] <= write_data;
end
[email protected](posedge clk or negedge rst_n) begin
if (!rst_n) begin
read_addr_e <= 'h0;
write_addr_e <= 'h0;
read_data <= 'h0;
end
else if (write_allow) begin
write_addr_e <= write_addr_e + 1;
end
else if (read_allow) begin
read_addr_e <= read_addr_e + 1;
read_data <= mem[read_addr];
end
end
// 空、满判断
assign empty = (write_addr_e == read_addr_e) ? 1 : 0;
assign full = ( (read_addr_e[4] != write_addr_e[4]) && (read_addr_e[3:0] == write_addr_e[3:0]) ) ? 1 : 0;
endmodule
异步 FIFO 的实现
在异步 FIFO 中,读写时钟不同,需要跨时钟域处理,还需要采用格雷码来打一拍,进行读写地址的采样(空满判断)。格雷码如下所示:
为什么采用格雷码?
事实上二进制读指针在增减时,经常发生多为突变,比如六位地址111111会在下一个 时刻变成000000,在实际电路中,这个变化过程要持续很长一段时间,会由 111111 经历 6 个状态转移到 00000,比如111111->101111->10111->100110->100100->000100->000000。由于写时钟与读时钟不同步,异步的写时钟很可能会在状态不稳定的中间某个状态采样,这样就会得到错误的读指针,进而做出错误的状态判断,导致系统异常(亚稳态问题)。而且由于多位同时突变,凭借概率论常识可知发生错误的可能性很大。
那么怎样才能避免这个问题的发生呢? 1000->1000(可能会错采样到它)->0000
显然,在中间状态采样,这个是不可能避免的,这是异步系统天生的缺陷。我们的目标是:即使在中间状态采样,也不能影响空满状态的判断。符合这个要求的编码方式是:每次只能有1个bit发生改变。为什么这么说呢?因为当只有一个bit发生改变时,即使在中间状态采样,其结果也不外乎两种:递增前原指针和递增后新指针。显然递增后新指针是最新情况的反映,如果采样到这个指针,那么和我们的设计预期是一致的,如果采样到递增前的原指针,会有什么结果呢?假设现在采样读指针,那么最坏的情况就是把“不满”判断成了 “满”,使得本来被允许的写操作被禁正了,但是这并不会对逻辑产生影响,只是带来了写操作的延迟。同样的,如果现在采样写指针,那么最坏的情况就是把“不空”判断成“空”,产生“虚空”, 使得本来被允许的读操作被禁止了,但是这也不会对逻辑产生影响,只是带来了读操作的延迟。
综上,使用格雷码进行采样是一种较好的选择。
硬件架构设计
异步FIFO的整体结构大致如下:
- Write_control:控制写操作与满信号(w_full)的判断与产生。
- Read_control:控制读操作与空信号(r_empty)的判断与产生。
- RAM:双端口数据存取RAM。
- Bin_to_gray:二进制码转格雷码模块。用于将读写地址二进制码转成格雷码。
- SYN:跨时钟同步模块,即将读地址的格雷码(r_g_addr)向w_clk同步;将写地址的格雷码(w_g_addr)向r_clk同步。主要操作就是通过寄存器打两拍。
module asyn_fifo #(
parameter WIDTH_D = 16,
parameter WIDTH_A = 8 ,
parameter DEPTH = 256
)
(
input r_clk,
input w_clk,
input rst_n,
input r_en,
input w_en,
input [WIDTH_D-1:0] data_in,
output [WIDTH_D-1:0] data_out
);
wire [WIDTH_A-1:0] w_addr_to_ram;
wire [WIDTH_A-1:0] r_addr_to_ram;
wire r_allow;
wire w_allow;
wire empty;
wire full;
RAM #(
.DEPTH(DEPTH),
.WIDTH_D(WIDTH_D),
.WIDTH_A(WIDTH_A)
)ram (
.clk_r (r_clk),
.clk_w (w_clk),
.rst_n (rst_n),
.write_en (w_allow),
.read_en (r_allow),
.write_data (data_in),
.write_addr (w_addr_to_ram),
.read_addr (r_addr_to_ram),
.read_data (data_out)
);
wire [WIDTH_A:0] w_gray1;
wire [WIDTH_A:0] w_gray2;
wire [WIDTH_A:0] r_gray1;
wire [WIDTH_A:0] r_gray2;
read_part #(
.WIDTH_D(WIDTH_D),
.WIDTH_A(WIDTH_A)
)read (
.r_clk (r_clk),
.rst_n (rst_n),
.r_en (r_en),
.w_gray (w_gray2),
.empty (empty),
.r_addr_to_ram (r_addr_to_ram),
.r_gray (r_gray1),
.r_allow (r_allow)
);
write_part #(
.WIDTH_D(WIDTH_D),
.WIDTH_A(WIDTH_A)
)write (
.w_clk (w_clk),
.rst_n (rst_n),
.w_en (w_en),
.r_gray (r_gray2),
.full (full),
.w_addr_to_ram (w_addr_to_ram),
.w_gray (w_gray1),
.w_allow (w_allow)
);
syn #(
.WIDTH_A(WIDTH_A)
)syn_1 (
.clk (r_clk),
.rst_n (rst_n),
.data_in (w_gray1),
.syn_data (w_gray2)
);
syn #(
.WIDTH_A(WIDTH_A)
)syn_2 (
.clk (w_clk),
.rst_n (rst_n),
.data_in (r_gray1),
.syn_data (r_gray2)
);
endmodule
module bin_to_gray#(
parameter WIDTH_A = 8
)
(
input [WIDTH_A:0] bin_c ,
output [WIDTH_A:0] gray_c
);
wire h_b = bin_c[WIDTH_A];
reg [WIDTH_A-1:0] gray_c_r;
integer i;
[email protected](*) begin
for (i = 0; i < WIDTH_A; i = i + 1)
gray_c_r[i] = bin_c[i] ^ bin_c[i + 1];
end
assign gray_c = {h_b, gray_c_r};
endmodule
module read_part#(
parameter WIDTH_D = 16,
parameter WIDTH_A = 8
)(
input r_clk ,
input rst_n ,
input r_en ,
input [WIDTH_A :0] w_gray ,
output empty ,
output [WIDTH_A-1:0] r_addr_to_ram ,
output reg [WIDTH_A :0] r_gray ,
output reg r_allow
);
reg [WIDTH_A:0] r_addr_r;
assign r_addr_ro_ram = r_addr_r[WIDTH_A-1:0]; // to RAM
[email protected](posedge r_clk or negedge rst_n) begin
if (!rst_n) begin
r_addr_r <= 'b0;
r_allow <= 'b0;
end
else if (r_en && !empty) begin
r_addr_r <= r_addr_r + 1;
r_allow <= 1;
end
end
assign empty = (w_gray[WIDTH_A:0] == r_gray[WIDTH_A:0]) ? 1 : 0;
wire [WIDTH_A:0] r_gray_w;
// 要通过寄存器打一拍之后再输出
[email protected](posedge r_clk or negedge rst_n) begin
if (!rst_n)
r_gray <= 'b0;
else
r_gray <= r_gray_w;
end
bin_to_gray bin1 (r_addr_r, r_gray_w);
endmodule
module write_part#(
parameter WIDTH_A = 8,
parameter WIDTH_D = 16
)
(
input w_clk ,
input rst_n ,
input w_en ,
input [WIDTH_A :0] r_gray ,
output [WIDTH_A-1:0] w_addr_to_ram,
output reg [WIDTH_A :0] w_gray ,
output full ,
output reg w_allow
);
reg [WIDTH_A:0] w_addr_r;
assign w_addr_to_ram = w_addr_r[WIDTH_A-1:0];
[email protected](posedge w_clk or negedge rst_n) begin
if (!rst_n) begin
w_addr_r <= 'b0;
w_allow <= 'b0;
end
else if (w_en && !full) begin
w_addr_r <= w_addr_r + 1;
w_allow <= 1;
end
end
assign full = ( {~w_gray[WIDTH_A], ~w_gray[WIDTH_A-1], w_gray[WIDTH_A-2:0]} == r_gray ) ? 1 : 0;
wire [WIDTH_A:0] w_gray_w;
[email protected](posedge w_clk or negedge rst_n) begin
if (!rst_n)
w_gray <= 'b0;
else
w_gray <= w_gray_w;
end
bin_to_gray bin2 (w_addr_r, w_gray_w);
endmodule
module syn #(
parameter WIDTH_A = 8
)
(
input clk ,
input rst_n ,
input [WIDTH_A:0] data_in ,
output [WIDTH_A:0] syn_data
);
reg [WIDTH_A:0] reg0, reg1;
[email protected](posedge clk or negedge rst_n) begin
if (!rst_n) begin
reg0 <= 'b0;
reg1 <= 'b0;
end
else begin
reg0 <= data_in;
reg1 <= reg0;
end
end
assign syn_data = reg1;
endmodule
module RAM #(
parameter DEPTH = 256,
parameter WIDTH_A = 8,
parameter WIDTH_D = 16
)
(
input clk_r ,
input clk_w ,
input rst_n ,
input write_en ,
input read_en ,
input [WIDTH_D-1:0] write_data ,
input [WIDTH_A-1:0] write_addr ,
input [WIDTH_A-1:0] read_addr ,
output reg [WIDTH_D-1:0] read_data
);
reg [WIDTH_D-1:0] memory [DEPTH-1:0];
integer i;
[email protected](posedge clk_w or negedge rst_n) begin: writing
if(!rst_n) begin
for (i = 0; i < DEPTH; i = i + 1)
memory[i] <= 'b0;
end
else if (write_en)
memory[write_addr] <= write_data;
end
[email protected](posedge clk_r or negedge rst_n) begin: reading
if(!rst_n) begin
read_data <= 'b0;
end
else if (read_en)
read_data <= memory[read_addr];
end
endmodule