第八章 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有效时,数据不会延后一个周期。
实验步骤
- 创建工程
- 添加IP核,IP Catelog中搜索fifo,选择FIFO Generator
- 设置IP核,读写始终不一致选择“Independent Clocks Block RAM”,还有等等其他的设置,根据需要设置进行设置即可,可以参考文档
- 根据读写时序编写测试代码。
实验过程
测试代码
写端口非满一直写,读端口非空一直读。并且进行错误统计。
`timescale 1ns / 1ps module fifo_test
( input sys_clk_p, // 系统差分时钟正端,200MHzinput 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; // 写时钟使用100MHzassign 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) beginif (!locked) begin// PLL未锁定时,保持复位状态wr_reset_sync <= 3'b0;end else begin// 使用移位寄存器实现同步,确保复位信号稳定wr_reset_sync <= {wr_reset_sync[1:0], 1'b1};endend// 读时钟域复位同步逻辑always @(posedge rd_clk or negedge locked) beginif (!locked) begin// PLL未锁定时,保持复位状态rd_reset_sync <= 3'b0;end else begin// 使用移位寄存器实现同步,确保复位信号稳定rd_reset_sync <= {rd_reset_sync[1:0], 1'b1};endend// 分配同步后的复位信号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) // 每次写使能有效时数据加1w_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_statealways @(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) beginif (!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) beginwrite_total <= write_total + 1'b1;endendend// 读时钟域错误监控always @(posedge rd_clk or negedge fifo_rd_rst_n) beginif (!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) beginread_total <= read_total + 1'b1;endendend// =========================================================================// 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为满的情况,但是不应该出现溢出的错误。
原因
wr_en_reg <= (write_state == W_FIFO) && !full;
时钟周期n: full = 0, wr_en = 1 → 开始写入
时钟周期n+1: full = 1 (FIFO变满),但wr_en仍然有效→ 在full=1时仍然写入 → overflow_error = 1
写使能信号会晚一个时钟周期拉低,满信号拉高一个周期后写使能才会拉低,会出现一次溢出错误。
解决方案
可以改为组合逻辑,但是有一些风险。
使用FIFO的将满信号来控制读取,可以将写使能提前一个信号拉低,正好解决问题。
同理,读使能也有类似的问题,出现下溢,改用将空信号控制。
常见问题和误区(来自DeepSeek)
1. 深度计算误区
误区:简单估算深度
// 错误:深度 = 写入速率 - 读取速率
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. 空满标志理解错误
误区:立即响应空满标志
// 错误理解:empty变低后立即可以读取
always @(posedge clk) beginif (!empty) begindata_out <= fifo_dout; // 可能读到无效数据!rd_en <= 1'b1;end
end// 正确:空标志变低后的下一个周期才能读取
always @(posedge clk) beginif (!empty) beginrd_en <= 1'b1; // 当前周期使能读取end else beginrd_en <= 1'b0;end
end// fifo_dout在rd_en有效后的下一个周期才有效
3. 复位处理不当
误区:复位期间继续操作
// 危险:复位期间没有停止读写
always @(posedge clk) beginwr_en <= data_valid; // 复位期间可能继续写入rd_en <= need_data; // 复位期间可能继续读取
end// 正确:复位时禁用所有操作
always @(posedge clk or posedge rst) beginif (rst) beginwr_en <= 1'b0;rd_en <= 1'b0;end else beginwr_en <= data_valid && !full;rd_en <= need_data && !empty;end
end
4. 跨时钟域FIFO的时钟关系
误区:任意时钟频率比
// 错误:认为任何时钟比都能工作
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. 读写使能控制错误
误区:持续使能读写
// 错误:不考虑空满状态
assign wr_en = data_valid; // 可能在全状态写入,数据丢失!
assign rd_en = data_needed; // 可能在空状态读取,读到垃圾数据!// 正确:受空满标志约束
assign wr_en = data_valid && !full;
assign rd_en = data_needed && !empty;
6. 几乎空/几乎满标志误用
误区:当作精确空满使用
// 错误理解
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. 数据宽度转换问题
误区:随意改变数据位宽
// 32位到8位FIFO,但写入模式错误
always @(posedge wr_clk) beginif (data_valid) begindin <= big_data; // 一次写入32位wr_en <= 1'b1;end
end// 读取端期望4个8位数据
always @(posedge rd_clk) beginif (!empty) beginsmall_data <= dout; // 每次读取8位rd_en <= 1'b1;end
end
// 问题:位宽不匹配导致数据错位!
8. 复位释放时机错误
误区:复位释放后立即操作
// 错误:复位释放后第一个周期就写入
always @(posedge clk) beginif (rst) begin// 复位中end else beginwr_en <= 1'b1; // 复位刚结束就写入,可能不稳定end
end// 正确:等待FIFO初始化完成
reg [2:0] reset_counter;
always @(posedge clk or posedge rst) beginif (rst) beginreset_counter <= 3'b0;wr_en <= 1'b0;end else beginif (reset_counter != 3'b111) beginreset_counter <= reset_counter + 1;wr_en <= 1'b0;end else beginwr_en <= data_valid && !full;endend
end
9. 背压机制设计错误
误区:简单的停止信号
// 简单但不高效的背压
assign stall_previous_stage = full; // 只在全满时停止// 更好的背压策略
assign flow_control = (fifo_usage > 80_PERCENT) ? SLOW_DOWN : (fifo_usage < 20_PERCENT) ? SPEED_UP : NORMAL;
10. 仿真 vs 实际行为差异
误区:仿真通过就认为正确
// 仿真可能工作的危险代码
initial begin#100; // 固定延迟start_test;
end// 实际硬件中的问题:
// - 空满标志的建立保持时间
// - 跨时钟域亚稳态
// - 复位毛刺
11. 状态监控缺失
误区:不监控FIFO健康状态
// 缺乏监控
fifo your_fifo (.wr_en(wr_en),.rd_en(rd_en)// 没有错误检测!
);// 应该添加:
reg overflow_error;
reg underflow_error;
reg almost_overflow;always @(posedge clk) beginoverflow_error <= wr_en && full;underflow_error <= rd_en && empty;almost_overflow <= (fifo_count > ALMOST_FULL_THRESHOLD);
end
12. 标准FIFO IP核配置误区
常见配置错误:
- 复位类型:选择异步复位但实际需要同步
- 使能信号:误配置为高有效/低有效
- 输出寄存器:忘记使能导致时序违规
- 握手信号:误用valid/ready协议
13. 性能优化误区
误区:过度优化
// 过早优化:复杂的流水线
always @(posedge clk) beginstage1 <= ...;stage2 <= stage1;stage3 <= stage2; // 增加了不必要的延迟
end// 应该先确保功能正确,再考虑优化
14. 测试覆盖不足
容易遗漏的测试场景:
- 边界情况:在深度-1和深度处操作
- 同时读写:空满边界的同时操作
- 时钟域交叉:极端频率比测试
- 复位恢复:运行中复位和恢复
- 背压测试:长时间背压下的行为
15. 调试技巧和最佳实践
添加调试监控:
// FIFO使用率监控
reg [31:0] write_count, read_count;
always @(posedge wr_clk) beginif (wr_en && !full) write_count <= write_count + 1;
end
always @(posedge rd_clk) beginif (rd_en && !empty) read_count <= read_count + 1;
end// 性能计数器
reg [31:0] full_count, empty_count;
always @(posedge wr_clk) beginif (full) full_count <= full_count + 1;
end
always @(posedge rd_clk) beginif (empty) empty_count <= empty_count + 1;
end
总结:FIFO设计检查清单
- 深度计算 ✓ 考虑最坏情况突发
- 时钟关系 ✓ 检查时钟频率比
- 复位策略 ✓ 同步复位,适当延迟
- 空满控制 ✓ 正确理解标志时序
- 错误处理 ✓ 溢出、下溢检测
- 性能监控 ✓ 使用率统计
- 充分测试 ✓ 覆盖边界情况
- 文档记录 ✓ 记录设计假设和限制
参考资料
- FIFO Generator v13.2(官方文档)
- Xilinx IP解析之FIFO Generator v13.2