FIFO详解
同步FIFO
同步fifo需要解决的主要问题是如何判断空和满,其余的设计比较简单
空满判断逻辑
解决方法是多用一位来充当空满判断位,如果地址位数为3(存储器中有8个存储单元),则地址位扩展后为4位,具体思想来自Clifford E. Cummings的论文
例如,刚开始的写地址为0000,当写满存储器8个单元后的写地址为1000,此时的读地址为0000,可以发现写地址和读地址的最高位相反,其余位相同,此时就是代表存储器满!同理,当读写地址所有位都相同时代表存储器空,因此可以得到存储器空满的判断逻辑代码:
// 空和满判断只能用组合逻辑,否则会滞后一拍
assign full = rst_n && (raddr == {~waddr[DEPTHPLUS], waddr[DEPTHPLUS-1:0]});
assign empty = rst_n && (raddr == waddr)
此时的方法只适用于存储器单元数为2的倍数,如果不是2的倍数需要采取其他方法,如使用一个多余的计数器来计数。
双端口存储器
双端口存储器的一个设计要点是有两个时钟,一个是写时钟,另一个是读时钟,使用两个不同的端口,具体代码为:
module dual_port_ram #(
parameter DEPTH = 64,
parameter WIDTH = 32
) (
input wclk,
input rclk,
input [WIDTH-1:0] wdata, // 写数据
input we, // 写使能信号
input re, // 读使能信号
input [$clog2(DEPTH)-1:0] raddr, // 写指针
input [$clog2(DEPTH)-1:0] waddr, // 读指针
output reg [WIDTH-1:0] rdata
);
reg [WIDTH-1:0] mem [0:DEPTH-1]; // 存储器的定义
always @(posedge wclk) begin
if (we)
mem[waddr] <= wdata;
end
always @(posedge rclk) begin
if (re)
rdata <= mem[raddr];
end
endmodule
同步FIFO模块的编写
同步FIFO比较简单,需要注意的事项为传给双端口存储器的写使能信号和读使能信号要与空满信号相联系,否则会读出错误的值
module syn_fifo #(
parameter DEPTH = 64,
parameter WIDTH = 32
) (
input clk,
input rst_n,
input [WIDTH-1:0] wdata,
input we, // 读使能
input re, // 写使能
output [WIDTH-1:0] rdata,
output full,
output empty
);
localparam DEPTHPLUS = $clog2(DEPTH);
// 用额外的一位来判断是空还是满
reg [DEPTHPLUS:0] waddr, raddr;
// 存储器的写使能和读使能信号
wire wen, ren;
dual_port_ram #(
.DEPTH(DEPTH),
.WIDTH(WIDTH)
) ram(
.wclk(clk),
.rclk(clk),
.waddr(waddr[DEPTHPLUS-1:0]),
.raddr(raddr[DEPTHPLUS-1:0]),
.we(wen),
.re(ren),
.rdata(rdata),
.wdata(wdata)
);
always @(posedge clk, negedge rst_n) begin
if (~rst_n)
waddr <= 0;
else if (we && ~full)
waddr <= waddr + 1;
else
waddr <= waddr;
end
always @(posedge clk, negedge rst_n) begin
if (~rst_n)
raddr <= 0;
else if (re && ~empty)
raddr <= raddr + 1;
else
raddr <= raddr;
end
// 空和满判断只能用组合逻辑,否则会滞后一拍
assign full = rst_n && (raddr == {~waddr[DEPTHPLUS], waddr[DEPTHPLUS-1:0]});
assign empty = rst_n && (raddr == waddr);
// 要确保读出的数据是正确的
assign wen = we & ~full;
assign ren = re & ~empty;
endmodule
异步FIFO
异步FIFO的设计难点有两个:
- 时钟偏差
- 空满逻辑判断
- 亚稳态问题
异步fifo的模块图如下图所示(来自Clifford E. Cummings的论文)
亚稳态
由于是异步的时钟,不能直接使用同步FIFO中所用的判断方法,因为是异步时钟,当一个信号跨越某个时钟域时会产生亚稳态问题,因为读写地址指针是来自不同时钟域,直接比较会产生亚稳态问题
亚稳态是指触发器无法在某个规定时间段内达到一个可确认的状态
解决方法就是引入时钟同步机制,一种简单方法是双触发器法,即上图中的两个同步模块:sync_r2w和sync_w2r
格雷码
如果直接用读写地址的二进制进行比较可能会出现问题,在采集同步信号时,由于时钟偏差的干扰,可能会采集到错误的值,如从3->4时(011->100)所有二进制位都发送了跳变,可能会产生8种中间态(000->111)
时钟偏差,Clock Skew,是指同一个时钟域内的时钟信号到达数字电路各个部分(一般是指寄存器)所用时间的差异
以下图为例,1号寄存器的时钟先到达,此时会产生111(7)这个中间态,如果wr_clk先于其余两个寄存器的rd_clk到达,此时同步寄存器会采集到错误的值7,而不是正确的值4
图源:https://cloud.tencent.com/developer/article/2115100
因此需要引入格雷码,格雷码的特点是相邻值的跳变只翻转一位,因此不会产生很多次态,同步端可能会采集到变化之前的值,不会采集到其他值,此时的时钟偏差带来的影响会减少到最小,如3->4(010->110)
格雷码的转换代码如下,本身与右移一位的结果异或得到格雷码
assign wr_addr_g = wr_addr ^ (wr_addr >> 1);
assign rd_addr_g = rd_addr ^ (rd_addr >> 1);
空满判断逻辑
此处使用格雷码来进行判断,仍然是将地址位扩展一位(原因和同步FIFO中一样),然后取其格雷码进行比较
假设存储器地址单元为8个,则地址位为3位,扩展1位到4位。如果读地址指针为0(0000),写地址指针为8(1000),此时代表存储器满。而0和8的格雷码是0000和1100,可以发现只有最高两位相反,其余位相同,此时可以得到空满判断逻辑
// 判断空满逻辑
// 1. 格雷码高两位相反,其余位相同为满
// 2. 格雷码全部位相同为空
// 3. 是与同步后的读写指针进行比较
// 此处是比较次态,用于下一次空满信号的产生
assign fifo_full_val = wr_addr_next_g == {~rd_addr_g_rr[ADDR_WIDTH:ADDR_WIDTH-1], rd_addr_g_rr[ADDR_WIDTH-2:0]};
assign fifo_empty_val = rd_addr_next_g == wr_addr_g_rr;
// 空满信号的产生
always @(posedge wr_clk, negedge wr_rst_n)
begin
if (~wr_rst_n)
fifo_full <= 1'b0;
else
fifo_full <= fifo_full_val;
end
always @(posedge rd_clk, negedge rd_rst_n)
begin
if (~rd_rst_n)
fifo_empty <= 1'b1;
else
fifo_empty <= fifo_empty_val;
end
注意到,代码中比较的是下一地址的格雷码和同步过来的信号,因为此处使用了时序逻辑,在下一时钟才更新空满信号的状态
或者可以使用组合逻辑,则此时的空满信号是通过现态的格雷码和同步的格雷码比较产生,代码修改如下
// 空满判断逻辑
// 此处是比较现态
// 对fifo_full和fifo_empty直接赋值,将时序逻辑改为组合逻辑,注意要将fifo_full和fifo_empty改为wire类型
assign fifo_full = wr_addr_g == {~rd_addr_g_rr[ADDR_WIDTH:ADDR_WIDTH-1], rd_addr_g_rr[ADDR_WIDTH-2:0]};
assign fifo_empty = rd_addr_g == wr_addr_g_rr;
异步FIFO所有代码
双端口存储器使用的是同步FIFO中的
module asy_fifo #(
parameter DEPTH = 64 ,
parameter WIDTH = 32
) (
//----------写控制端---------------
input wr_clk ,
input wr_rst_n ,
input wr_en ,
output reg fifo_full ,
input [WIDTH-1:0] wr_data ,
//----------读控制段---------------
input rd_clk ,
input rd_rst_n ,
input rd_en ,
output reg fifo_empty ,
output [WIDTH-1:0] rd_data
);
localparam ADDR_WIDTH = $clog2(DEPTH);
// 读写指针,用于访存
reg [ADDR_WIDTH:0] wr_addr;
reg [ADDR_WIDTH:0] rd_addr;
// 延迟一拍和两拍的读写指针的格雷码
reg [ADDR_WIDTH:0] wr_addr_g_r, wr_addr_g_rr;
reg [ADDR_WIDTH:0] rd_addr_g_r, rd_addr_g_rr;
// 当前读写地址的格雷码
reg [ADDR_WIDTH:0] wr_addr_g;
reg [ADDR_WIDTH:0] rd_addr_g;
// 下一读写地址的格雷码
wire [ADDR_WIDTH:0] wr_addr_next_g;
wire [ADDR_WIDTH:0] rd_addr_next_g;
// 下一周期的写读地址
wire [ADDR_WIDTH:0] wr_addr_next, rd_addr_next;
// 双端口存储器的写使能和读使能
wire wen;
wire ren;
// 双端口存储器实例
dual_port_ram #(
.DEPTH(DEPTH),
.WIDTH(WIDTH)
) U1(
.wr_clk(wr_clk),
.rd_clk(rd_clk),
.wr_data(wr_data),
.wen(wen),
.ren(ren),
.rd_addr(rd_addr[ADDR_WIDTH-1:0]),
.wr_addr(wr_addr[ADDR_WIDTH-1:0]),
.rd_data(rd_data)
);
// 二进制码右移 1 位后与本身异或,其结果就是格雷码
assign wr_addr_next_g = wr_addr_next ^ (wr_addr_next >> 1);
assign rd_addr_next_g = rd_addr_next ^ (rd_addr_next >> 1);
// 下一读写地址的产生逻辑
assign wr_addr_next = wr_addr + (~fifo_full & wr_en);
assign rd_addr_next = rd_addr + (~fifo_empty & rd_en);
// 判断空满逻辑
// 1. 格雷码高两位相反,其余位相同为满
// 2. 格雷码全部位相同为空
// 3. 是与同步后的读写指针进行比较
// 此处是比较次态,用于下一次空满信号的产生
assign fifo_full_val = wr_addr_next_g == {~rd_addr_g_rr[ADDR_WIDTH:ADDR_WIDTH-1], rd_addr_g_rr[ADDR_WIDTH-2:0]};
assign fifo_empty_val = rd_addr_next_g == wr_addr_g_rr;
// 控制双端口存储器的读写
assign wen = wr_en & ~fifo_full;
assign ren = rd_en & ~fifo_empty;
// 空满信号的产生
always @(posedge wr_clk, negedge wr_rst_n)
begin
if (~wr_rst_n)
fifo_full <= 1'b0;
else
fifo_full <= fifo_full_val;
end
always @(posedge rd_clk, negedge rd_rst_n)
begin
if (~rd_rst_n)
fifo_empty <= 1'b1;
else
fifo_empty <= fifo_empty_val;
end
// 地址更新和格雷码更新
always @(posedge wr_clk, negedge wr_rst_n)
begin
if (~wr_rst_n)
{wr_addr, wr_addr_g} <= 0;
else
{wr_addr, wr_addr_g} <= {wr_addr_next, wr_addr_next_g};
end
always @(posedge rd_clk, negedge rd_rst_n)
begin
if (~rd_rst_n)
{rd_addr, rd_addr_g} <= 0;
else
{rd_addr, rd_addr_g} <= {rd_addr_next, rd_addr_next_g};
end
// 两拍延迟,用于时钟同步
always @(posedge rd_clk, negedge rd_rst_n)
begin
if (~rd_rst_n)
{wr_addr_g_rr, wr_addr_g_r} <= 0;
else
{wr_addr_g_rr, wr_addr_g_r} <= {wr_addr_g_r, wr_addr_g};
end
always @(posedge wr_clk, negedge wr_rst_n)
begin
if (~wr_rst_n)
{rd_addr_g_rr, rd_addr_g_r} <= 0;
else
{rd_addr_g_rr, rd_addr_g_r} <= {rd_addr_g_r, rd_addr_g};
end
endmodule