第八章 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",还有等等其他的设置,根据需要设置进行设置即可,可以参考文档
- 根据读写时序编写测试代码。
实验过程
测试代码
写端口非满一直写,读端口非空一直读。并且进行错误统计。
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设计检查清单
- 深度计算 ✓ 考虑最坏情况突发
- 时钟关系 ✓ 检查时钟频率比
- 复位策略 ✓ 同步复位,适当延迟
- 空满控制 ✓ 正确理解标志时序
- 错误处理 ✓ 溢出、下溢检测
- 性能监控 ✓ 使用率统计
- 充分测试 ✓ 覆盖边界情况
- 文档记录 ✓ 记录设计假设和限制
参考资料
- FIFO Generator v13.2(官方文档)
- Xilinx IP解析之FIFO Generator v13.2