FIFO学习笔记

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的设计难点有两个:

  1. 时钟偏差
  2. 空满逻辑判断
  3. 亚稳态问题

异步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
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
你好!关于学习数据结构的C语言笔记,我可以给你一些基本的指导和概念。数据结构是计算机科学中非常重要的一门课程,它涉及存储和组织数据的方法。C语言是一种常用的编程语言,很适合用于实现各种数据结构。 下面是一些数据结构的基本概念,你可以在学习笔记中包含它们: 1. 数组(Array):一种线性数据结构,可以存储相同类型的元素。在C语言中,数组是通过索引访问的。 2. 链表(Linked List):也是一种线性数据结构,但不需要连续的内存空间。链表由节点组成,每个节点包含数据和指向下一个节点的指针。 3. 栈(Stack):一种后进先出(LIFO)的数据结构,类似于装满物品的箱子。在C语言中,可以使用数组或链表来实现栈。 4. 队列(Queue):一种先进先出(FIFO)的数据结构,类似于排队等候的队伍。同样可以使用数组或链表来实现队列。 5. 树(Tree):一种非线性数据结构,由节点和边组成。每个节点可以有多个子节点。二叉树是一种特殊的树结构,每个节点最多有两个子节点。 6. 图(Graph):另一种非线性数据结构,由节点和边组成。图可以用来表示各种实际问题,如社交网络和地图。 这只是数据结构中的一些基本概念,还有其他更高级的数据结构,如堆、哈希表和二叉搜索树等。在学习笔记中,你可以介绍每个数据结构的定义、操作以及适合使用它们的场景。 希望这些信息对你有所帮助!如果你有任何进一步的问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值