第八章 FPGA 片内 FIFO 读写测试实验

第八章 FPGA 片内 FIFO 读写测试实验

实验原理

FIFO: First in, First out代表先进的数据先出 ,后进的数据后出。

只需通过 IP 核例化一个 FIFO ,根据 FIFO 的读写时序来写入和读取FIFO 中存储的数据 。

FIFO 的典型结构如下,主要分为读和写两部分,另外就是状态信号,空和满信号,同时还有数据的数量状态信号,与 RAM 最大的不同是 FIFO 没有地址线,不能进行随机地址读取数据。

标准FIFO读写时序,都是在时钟的上升沿进行操作。

写使能(wr_en)为高时写入FIFO数据,将满信号(almost_full)在只能写入一个数据时有效,写入后FIFO满信号(full)拉高,在full的情况下继续向FIFO写数据将会产生溢出信号(overflow)。

读使能(rd_en)为高时读取FIFO数据,数据在下一个周期有效,valid为数据有效信号。将空信号(almost_empty)表示只剩一个数据可读时有效,读取后空信号(empty)有效,继续读的话会产生下溢信号(underflow)。

FWFT模式(数据预取模式,提前取出一个数据)在rd_en有效时,数据不会延后一个周期。

实验步骤

  1. 创建工程
  2. 添加IP核,IP Catelog中搜索fifo,选择FIFO Generator
  3. 设置IP核,读写始终不一致选择"Independent Clocks Block RAM",还有等等其他的设置,根据需要设置进行设置即可,可以参考文档
  4. 根据读写时序编写测试代码。

实验过程

测试代码

写端口非满一直写,读端口非空一直读。并且进行错误统计。

c 复制代码
`timescale 1ns / 1ps 

module fifo_test 
( 
    input       sys_clk_p,            // 系统差分时钟正端,200MHz
    input       sys_clk_n,            // 系统差分时钟负端,200MHz  
    input       rst_n                 // 全局复位信号,低电平有效  
);  
 
    // FIFO控制信号
    reg  [15:0]     w_data;           // 写入FIFO的数据
    wire            wr_en;            // FIFO写使能信号
    wire            rd_en;            // FIFO读使能信号
    wire [15:0]     r_data;           // 从FIFO读出的数据
    wire            full;             // FIFO满标志信号
    wire            almost_full;      // FIFO将满标志信号
    wire            empty;            // FIFO空标志信号
    wire            almost_empty;     // FIFO将空标志信号
    wire [8:0]      rd_data_count;    // 可读数据个数计数器
    wire [8:0]      wr_data_count;    // 已写入数据个数计数器
    
    // 时钟和复位信号
    wire            clk_100M;         // PLL生成的100MHz时钟
    wire            clk_75M;          // PLL生成的75MHz时钟
    wire            locked;           // PLL锁定信号,锁定后为高电平
    wire            fifo_wr_rst_n;    // FIFO写时钟域复位信号,低电平有效
    wire            fifo_rd_rst_n;    // FIFO读时钟域复位信号,低电平有效
    
    wire            wr_clk;           // FIFO写时钟
    wire            rd_clk;           // FIFO读时钟
    reg [7:0]       wcnt;             // 写FIFO复位等待计数器
    reg [7:0]       rcnt;             // 读FIFO复位等待计数器
    
    // 错误监控和统计信号
    reg             overflow_error;   // 溢出错误标志:在FIFO满时仍然写入
    reg             underflow_error;  // 下溢错误标志:在FIFO空时仍然读取
    reg [31:0]      write_total;      // 总写入数据计数
    reg [31:0]      read_total;       // 总读取数据计数

    // =========================================================================
    // PLL时钟管理模块实例化
    // 功能:将输入的200MHz差分时钟转换为100MHz和75MHz单端时钟
    // =========================================================================
    clk_wiz_0 fifo_pll 
    ( 
        // 时钟输出端口
        .clk_out1(clk_100M),          // 输出100MHz时钟
        .clk_out2(clk_75M),           // 输出75MHz时钟
        // 状态和控制信号
        .reset(~rst_n),               // 复位信号,高电平有效(所以取反外部复位)
        .locked(locked),              // PLL锁定输出,锁定后为高电平
        // 时钟输入端口(差分输入)
        .clk_in1_p(sys_clk_p),        // 差分时钟正端输入
        .clk_in1_n(sys_clk_n)         // 差分时钟负端输入
    );         

    // 时钟分配
    assign wr_clk = clk_100M;         // 写时钟使用100MHz
    assign rd_clk = clk_75M;          // 读时钟使用75MHz
 
    // =========================================================================
    // 同步复位信号生成
    // 重要:为避免跨时钟域问题,需要为每个时钟域生成独立的同步复位信号
    // =========================================================================
    reg [2:0] wr_reset_sync = 3'b0;   // 写时钟域复位同步寄存器
    reg [2:0] rd_reset_sync = 3'b0;   // 读时钟域复位同步寄存器
    
    // 写时钟域复位同步逻辑
    always @(posedge wr_clk or negedge locked) begin
        if (!locked) begin
            // PLL未锁定时,保持复位状态
            wr_reset_sync <= 3'b0;
        end else begin
            // 使用移位寄存器实现同步,确保复位信号稳定
            wr_reset_sync <= {wr_reset_sync[1:0], 1'b1};
        end
    end
    
    // 读时钟域复位同步逻辑
    always @(posedge rd_clk or negedge locked) begin
        if (!locked) begin
            // PLL未锁定时,保持复位状态
            rd_reset_sync <= 3'b0;
        end else begin
            // 使用移位寄存器实现同步,确保复位信号稳定
            rd_reset_sync <= {rd_reset_sync[1:0], 1'b1};
        end
    end
    
    // 分配同步后的复位信号
    assign fifo_wr_rst_n = wr_reset_sync[2];  // 取同步链的最后一级作为稳定复位
    assign fifo_rd_rst_n = rd_reset_sync[2];

    // =========================================================================
    // 写FIFO状态机
    // 功能:控制FIFO的写入时序,确保复位后稳定工作
    // =========================================================================
    // 状态定义
    localparam [1:0] W_IDLE = 2'b01;  // 空闲状态:等待复位稳定
    localparam [1:0] W_FIFO = 2'b10;  // 写入状态:持续向FIFO写入数据
 
    reg [1:0] write_state;            // 当前写状态
    reg [1:0] next_write_state;       // 下一写状态
 
    // 写状态机时序逻辑
    always @(posedge wr_clk or negedge fifo_wr_rst_n) 
    begin  
        if (!fifo_wr_rst_n) 
            // 复位时进入空闲状态
            write_state <= W_IDLE; 
        else 
            // 正常工作时状态转移
            write_state <= next_write_state; 
    end 
 
    // 写状态机组合逻辑
    always @(*) 
    begin 
        case (write_state) 
            W_IDLE: 
            begin 
                // 等待80个时钟周期确保系统稳定,然后进入写入状态
                if (wcnt == 8'd79)               
                    next_write_state <= W_FIFO; 
                else 
                    next_write_state <= W_IDLE; 
            end 
            W_FIFO: 
                // 进入写入状态后保持,持续写入数据
                next_write_state <= W_FIFO;      
            default: 
                // 默认返回空闲状态
                next_write_state <= W_IDLE; 
        endcase 
    end 

    // 写状态机等待计数器
    always @(posedge wr_clk or negedge fifo_wr_rst_n) 
    begin  
        if (!fifo_wr_rst_n) 
            // 复位时清零计数器
            wcnt <= 8'd0; 
        else if (write_state == W_IDLE) 
            // 在空闲状态时递增计数器
            wcnt <= wcnt + 1'b1; 
        else 
            // 进入写入状态后清零计数器
            wcnt <= 8'd0; 
    end 

    // =========================================================================
    // 写使能和数据生成逻辑
    // =========================================================================
    // 写使能信号生成(寄存器输出避免毛刺)
    reg wr_en_reg;
    always @(posedge wr_clk or negedge fifo_wr_rst_n) 
    begin 
        if (!fifo_wr_rst_n) 
            wr_en_reg <= 1'b0; 
        else 
            // 在写入状态且FIFO不满时产生写使能
            wr_en_reg <= (write_state == W_FIFO) && !almost_full; 
    end 
    assign wr_en = wr_en_reg;
    
    // 写入数据生成:简单的递增计数器
    always @(posedge wr_clk or negedge fifo_wr_rst_n) 
    begin 
        if (!fifo_wr_rst_n) 
            // 复位时从1开始
            w_data <= 16'd1; 
        else if (wr_en) 
            // 每次写使能有效时数据加1
            w_data <= w_data + 1'b1; 
    end 
 
    // =========================================================================
    // 读FIFO状态机
    // 功能:控制FIFO的读取时序,确保复位后稳定工作
    // =========================================================================
    // 状态定义
    localparam [1:0] R_IDLE = 2'b01;  // 空闲状态:等待复位稳定
    localparam [1:0] R_FIFO = 2'b10;  // 读取状态:持续从FIFO读取数据
    
    reg [1:0] read_state;             // 当前读状态
    reg [1:0] next_read_state;        // 下一读状态
 
    // 读状态机时序逻辑
    always @(posedge rd_clk or negedge fifo_rd_rst_n) 
    begin 
        if (!fifo_rd_rst_n) 
            // 复位时进入空闲状态
            read_state <= R_IDLE; 
        else 
            // 正常工作时状态转移
            read_state <= next_read_state; 
    end 
 
    // 读状态机组合逻辑
    always @(*) 
    begin 
        case (read_state) 
            R_IDLE: 
            begin 
                // 等待60个时钟周期确保系统稳定,然后进入读取状态
                if (rcnt == 8'd59)              
                    next_read_state <= R_FIFO; 
                else 
                    next_read_state <= R_IDLE; 
            end 
            R_FIFO:  
                // 进入读取状态后保持,持续读取数据
                next_read_state <= R_FIFO;      
            default: 
                // 默认返回空闲状态
                next_read_state <= R_IDLE; 
        endcase 
    end 
 
    // 读状态机等待计数器
    // 重要修改:修复了原代码中的跨时钟域问题,现在使用正确的read_state
    always @(posedge rd_clk or negedge fifo_rd_rst_n) 
    begin  
        if (!fifo_rd_rst_n) 
            // 复位时清零计数器
            rcnt <= 8'd0; 
        else if (read_state == R_IDLE) 
            // 在空闲状态时递增计数器
            rcnt <= rcnt + 1'b1; 
        else 
            // 进入读取状态后清零计数器
            rcnt <= 8'd0; 
    end

    // =========================================================================
    // 读使能信号生成
    // =========================================================================
    // 读使能信号生成(寄存器输出避免毛刺)
    reg rd_en_reg;
    always @(posedge rd_clk or negedge fifo_rd_rst_n) 
    begin 
        if (!fifo_rd_rst_n) 
            rd_en_reg <= 1'b0; 
        else 
            // 在读取状态且FIFO不空时产生读使能
            rd_en_reg <= (read_state == R_FIFO) && !almost_empty; 
    end 
    assign rd_en = rd_en_reg;

    // =========================================================================
    // 错误监控和统计逻辑
    // 功能:检测FIFO操作中的错误并统计性能数据
    // =========================================================================
    
    // 写时钟域错误监控
    always @(posedge wr_clk or negedge fifo_wr_rst_n) begin
        if (!fifo_wr_rst_n) begin
            // 复位时清零错误标志和计数器
            overflow_error <= 1'b0;
            write_total <= 32'b0;
        end else begin
            // 检测溢出错误:在FIFO满时仍然尝试写入
            overflow_error <= wr_en && full;
            
            // 统计成功写入的数据总数
            if (wr_en && !full) begin
                write_total <= write_total + 1'b1;
            end
        end
    end
    
    // 读时钟域错误监控
    always @(posedge rd_clk or negedge fifo_rd_rst_n) begin
        if (!fifo_rd_rst_n) begin
            // 复位时清零错误标志和计数器
            underflow_error <= 1'b0;
            read_total <= 32'b0;
        end else begin
            // 检测下溢错误:在FIFO空时仍然尝试读取
            underflow_error <= rd_en && empty;
            
            // 统计成功读取的数据总数
            if (rd_en && !empty) begin
                read_total <= read_total + 1'b1;
            end
        end
    end

    // =========================================================================
    // FIFO IP核实例化
    // 重要:FIFO IP核的复位通常是高电平有效,所以需要将我们的低有效复位取反
    // =========================================================================
    fifo_ip fifo_ip_inst  
    ( 
        .rst            (~fifo_wr_rst_n),       // 复位信号,高电平有效(对FIFO IP取反)
        .wr_clk         (wr_clk),               // 写时钟输入
        .rd_clk         (rd_clk),               // 读时钟输入
        .din            (w_data),               // 写入数据输入 [15:0]
        .wr_en          (wr_en),                // 写使能输入
        .rd_en          (rd_en),                // 读使能输入
        .dout           (r_data),               // 读取数据输出 [15:0]
        .full           (full),                 // 满标志输出
        .almost_full    (almost_full),          // 将满标志输出
        .empty          (empty),                // 空标志输出
        .almost_empty   (almost_empty),         // 将空标志输出
        .rd_data_count  (rd_data_count),        // 可读数据计数输出 [8:0]
        .wr_data_count  (wr_data_count)         // 已写数据计数输出 [8:0]
    ); 
    
    // =========================================================================
    // 逻辑分析仪(ILA)调试接口
    // 功能:用于在线调试和信号观测
    // =========================================================================
    
    // 写通道逻辑分析仪
    ila_fifo ila_wfifo ( 
        .clk(wr_clk),                  // 采样时钟:写时钟
        .probe0(w_data),               // 探针0:写入数据
        .probe1(wr_en),                // 探针1:写使能信号
        .probe2(full),                 // 探针2:满标志
        .probe3(wr_data_count),        // 探针3:写数据计数
        .probe4(overflow_error),       // 探针4:溢出错误标志
        .probe5(write_total[15:0])     // 探针5:写入总数(低16位)
    ); 
    
    // 读通道逻辑分析仪
    ila_fifo ila_rfifo ( 
        .clk(rd_clk),                  // 采样时钟:读时钟
        .probe0(r_data),               // 探针0:读取数据
        .probe1(rd_en),                // 探针1:读使能信号
        .probe2(empty),                // 探针2:空标志
        .probe3(rd_data_count),        // 探针3:读数据计数
        .probe4(underflow_error),      // 探针4:下溢错误标志
        .probe5(read_total[15:0])      // 探针5:读取总数(低16位)
    ); 

endmodule

ILA IP核配置

在线调试

读的数据都是连续的,这里没有空的情况,因为写的速率较快。

使能在将满时拉低,停止数据写入。

问题记录

1. 写数据时产生overflow_error错误。

现象:

写数据的速率要大于读数据的速率,会出现fifo为满的情况,但是不应该出现溢出的错误。
原因

verilog 复制代码
wr_en_reg <= (write_state == W_FIFO) && !full;
text 复制代码
时钟周期n:   full = 0, wr_en = 1  → 开始写入
时钟周期n+1: full = 1 (FIFO变满),但wr_en仍然有效
             → 在full=1时仍然写入 → overflow_error = 1

写使能信号会晚一个时钟周期拉低,满信号拉高一个周期后写使能才会拉低,会出现一次溢出错误。

解决方案

可以改为组合逻辑,但是有一些风险。

使用FIFO的将满信号来控制读取,可以将写使能提前一个信号拉低,正好解决问题。

同理,读使能也有类似的问题,出现下溢,改用将空信号控制。

常见问题和误区(来自DeepSeek)

1. 深度计算误区

误区:简单估算深度

verilog 复制代码
// 错误:深度 = 写入速率 - 读取速率
localparam FIFO_DEPTH = 100; // 随意选择

// 正确:考虑最坏情况的突发
// 计算公式:深度 ≥ (写入速率 × 突发长度) / 读取速率
localparam BURST_LENGTH = 128;
localparam WR_RATE = 100; // MHz
localparam RD_RATE = 50;  // MHz
localparam FIFO_DEPTH = (WR_RATE * BURST_LENGTH) / RD_RATE + 1; // 257

正确的深度计算考虑因素:

  • 最大突发数据量
  • 读写时钟频率比
  • 背压机制延迟
  • 安全余量(通常+20%)

2. 空满标志理解错误

误区:立即响应空满标志

verilog 复制代码
// 错误理解:empty变低后立即可以读取
always @(posedge clk) begin
    if (!empty) begin
        data_out <= fifo_dout;  // 可能读到无效数据!
        rd_en <= 1'b1;
    end
end

// 正确:空标志变低后的下一个周期才能读取
always @(posedge clk) begin
    if (!empty) begin
        rd_en <= 1'b1;          // 当前周期使能读取
    end else begin
        rd_en <= 1'b0;
    end
end

// fifo_dout在rd_en有效后的下一个周期才有效

3. 复位处理不当

误区:复位期间继续操作

verilog 复制代码
// 危险:复位期间没有停止读写
always @(posedge clk) begin
    wr_en <= data_valid;  // 复位期间可能继续写入
    rd_en <= need_data;   // 复位期间可能继续读取
end

// 正确:复位时禁用所有操作
always @(posedge clk or posedge rst) begin
    if (rst) begin
        wr_en <= 1'b0;
        rd_en <= 1'b0;
    end else begin
        wr_en <= data_valid && !full;
        rd_en <= need_data && !empty;
    end
end

4. 跨时钟域FIFO的时钟关系

误区:任意时钟频率比

verilog 复制代码
// 错误:认为任何时钟比都能工作
fifo_async your_instance (
  .rst(rst),        // 异步复位,危险!
  .wr_clk(clk_200M),
  .rd_clk(clk_1M),  // 200:1 的时钟比可能有问题
  .din(din),
  .wr_en(wr_en),
  .rd_en(rd_en),
  .dout(dout),
  .full(full),
  .empty(empty)
);

异步FIFO的限制:

  • 通常建议读写时钟比 < 10:1
  • 极端时钟比需要特殊设计
  • 复位必须是写时钟域的同步复位

5. 读写使能控制错误

误区:持续使能读写

verilog 复制代码
// 错误:不考虑空满状态
assign wr_en = data_valid;  // 可能在全状态写入,数据丢失!
assign rd_en = data_needed; // 可能在空状态读取,读到垃圾数据!

// 正确:受空满标志约束
assign wr_en = data_valid && !full;
assign rd_en = data_needed && !empty;

6. 几乎空/几乎满标志误用

误区:当作精确空满使用

verilog 复制代码
// 错误理解
if (almost_full) stop_writing_entirely;  // 过早停止
if (almost_empty) start_reading_franticly; // 过度反应

// 正确使用
if (almost_full) reduce_writing_gradually;  // 渐进调整
if (almost_empty) request_more_data_soon;   // 提前预警

7. 数据宽度转换问题

误区:随意改变数据位宽

verilog 复制代码
// 32位到8位FIFO,但写入模式错误
always @(posedge wr_clk) begin
    if (data_valid) begin
        din <= big_data;     // 一次写入32位
        wr_en <= 1'b1;
    end
end

// 读取端期望4个8位数据
always @(posedge rd_clk) begin
    if (!empty) begin
        small_data <= dout;  // 每次读取8位
        rd_en <= 1'b1;
    end
end
// 问题:位宽不匹配导致数据错位!

8. 复位释放时机错误

误区:复位释放后立即操作

verilog 复制代码
// 错误:复位释放后第一个周期就写入
always @(posedge clk) begin
    if (rst) begin
        // 复位中
    end else begin
        wr_en <= 1'b1;  // 复位刚结束就写入,可能不稳定
    end
end

// 正确:等待FIFO初始化完成
reg [2:0] reset_counter;
always @(posedge clk or posedge rst) begin
    if (rst) begin
        reset_counter <= 3'b0;
        wr_en <= 1'b0;
    end else begin
        if (reset_counter != 3'b111) begin
            reset_counter <= reset_counter + 1;
            wr_en <= 1'b0;
        end else begin
            wr_en <= data_valid && !full;
        end
    end
end

9. 背压机制设计错误

误区:简单的停止信号

verilog 复制代码
// 简单但不高效的背压
assign stall_previous_stage = full;  // 只在全满时停止

// 更好的背压策略
assign flow_control = (fifo_usage > 80_PERCENT) ? SLOW_DOWN : 
                     (fifo_usage < 20_PERCENT) ? SPEED_UP : NORMAL;

10. 仿真 vs 实际行为差异

误区:仿真通过就认为正确

verilog 复制代码
// 仿真可能工作的危险代码
initial begin
    #100; // 固定延迟
    start_test;
end

// 实际硬件中的问题:
// - 空满标志的建立保持时间
// - 跨时钟域亚稳态
// - 复位毛刺

11. 状态监控缺失

误区:不监控FIFO健康状态

verilog 复制代码
// 缺乏监控
fifo your_fifo (
    .wr_en(wr_en),
    .rd_en(rd_en)
    // 没有错误检测!
);

// 应该添加:
reg overflow_error;
reg underflow_error;
reg almost_overflow;

always @(posedge clk) begin
    overflow_error <= wr_en && full;
    underflow_error <= rd_en && empty;
    almost_overflow <= (fifo_count > ALMOST_FULL_THRESHOLD);
end

12. 标准FIFO IP核配置误区

常见配置错误:

  • 复位类型:选择异步复位但实际需要同步
  • 使能信号:误配置为高有效/低有效
  • 输出寄存器:忘记使能导致时序违规
  • 握手信号:误用valid/ready协议

13. 性能优化误区

误区:过度优化

verilog 复制代码
// 过早优化:复杂的流水线
always @(posedge clk) begin
    stage1 <= ...;
    stage2 <= stage1;
    stage3 <= stage2;  // 增加了不必要的延迟
end

// 应该先确保功能正确,再考虑优化

14. 测试覆盖不足

容易遗漏的测试场景:

  • 边界情况:在深度-1和深度处操作
  • 同时读写:空满边界的同时操作
  • 时钟域交叉:极端频率比测试
  • 复位恢复:运行中复位和恢复
  • 背压测试:长时间背压下的行为

15. 调试技巧和最佳实践

添加调试监控:

verilog 复制代码
// FIFO使用率监控
reg [31:0] write_count, read_count;
always @(posedge wr_clk) begin
    if (wr_en && !full) write_count <= write_count + 1;
end
always @(posedge rd_clk) begin
    if (rd_en && !empty) read_count <= read_count + 1;
end

// 性能计数器
reg [31:0] full_count, empty_count;
always @(posedge wr_clk) begin
    if (full) full_count <= full_count + 1;
end
always @(posedge rd_clk) begin
    if (empty) empty_count <= empty_count + 1;
end

总结:FIFO设计检查清单

  • 深度计算 ✓ 考虑最坏情况突发
  • 时钟关系 ✓ 检查时钟频率比
  • 复位策略 ✓ 同步复位,适当延迟
  • 空满控制 ✓ 正确理解标志时序
  • 错误处理 ✓ 溢出、下溢检测
  • 性能监控 ✓ 使用率统计
  • 充分测试 ✓ 覆盖边界情况
  • 文档记录 ✓ 记录设计假设和限制

参考资料

  1. FIFO Generator v13.2(官方文档)
  2. Xilinx IP解析之FIFO Generator v13.2
相关推荐
cycf8 小时前
以太网接口(一)
fpga开发
nnerddboy1 天前
FPGA自学笔记(正点原子ZYNQ7020):1.Vivado软件安装与点灯
笔记·fpga开发
li星野2 天前
打工人日报#20251005
笔记·程序人生·fpga开发·学习方法
通信小呆呆2 天前
FPGA 上的 OFDM 同步:从 S&C 到残差 CFO 的工程化实现
fpga开发·信号处理·同步·ofdm
hahaha60162 天前
高层次综合基础-vivado hls第三章
算法·fpga开发
XINVRY-FPGA5 天前
XCVU9P-2FLGA2104E Xilinx AMD Virtex UltraScale+ FPGA
人工智能·嵌入式硬件·fpga开发·硬件工程·dsp开发·射频工程·fpga
范纹杉想快点毕业5 天前
ZYNQ7045芯片中UART实现RS422通信详解,50000字解析,C语言,嵌入式开发,软件开发
c语言·笔记·stm32·单片机·嵌入式硬件·mcu·fpga开发
千宇宙航7 天前
闲庭信步使用图像验证平台加速FPGA的开发:第三十课——车牌识别的FPGA实现(2)实现车牌定位
图像处理·计算机视觉·fpga开发·车牌识别
灵风_Brend7 天前
最大最小延时约束
fpga开发