3、同步FIFO设计:单时钟域下的FIFO架构、读写逻辑、空满判断
好,咱们今天聊聊同步FIFO。说白了,就是读写两边用同一个时钟的FIFO。你可能会问:这有什么难的?不就是个双端口RAM加几个计数器吗?嗯,表面上看确实是这样,但真正做起来,坑可不少。
我记得刚入行那会儿,第一次写FIFO,觉得太简单了。结果仿真一跑,空满标志老是出问题。后来被老工程师拎着耳朵教了一通,才明白这里面的门道。今天我就把这些经验掰开了讲给你听。
3.1 同步FIFO的基本架构
同步FIFO的核心部件其实就三个:
- 存储体:双端口RAM,一个口写、一个口读
- 写指针:指向下一个要写入的地址
- 读指针:指向下一个要读出的地址
你想想看,数据从写端口进来,存到RAM里。读端口再从RAM里把数据取走。写指针和读指针就像两个人在赛跑——写指针追读指针,读指针追写指针。谁跑得快,决定了FIFO是满还是空。
关键点:同步FIFO的读写指针都在同一个时钟域下,所以不需要跨时钟域同步处理。这是它和异步FIFO最大的区别。
3.2 读写逻辑设计
写逻辑其实不复杂。当写使能有效且FIFO不满时,就把数据写入当前写指针指向的地址,然后写指针加1。读逻辑类似,读使能有效且FIFO不空时,从读指针指向的地址读出数据,读指针加1。
代码写出来大概是这样:
// 写逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
wr_ptr <= 0;
else if (wr_en && !full)
wr_ptr <= wr_ptr + 1;
end
// 读逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
rd_ptr <= 0;
else if (rd_en && !empty)
rd_ptr <= rd_ptr + 1;
end
这里有个细节我特别想提醒你:写使能和读使能不能同时有效吗? 当然可以!同步FIFO允许同时读写,只要地址不冲突就行。双端口RAM天然支持这个操作。
3.3 空满判断——最容易被坑的地方
好,重点来了。空满判断是FIFO设计的灵魂,也是新手最容易翻车的地方。
判断空很简单:读指针追上写指针,就是空了。也就是 rd_ptr == wr_ptr。
判断满呢?如果写指针追上读指针,就是满了。但问题来了——空和满的判断条件都是 rd_ptr == wr_ptr,你怎么区分?
嗯,这里有个经典做法:把指针多扩展一位。
什么意思?假设FIFO深度是16,地址位宽需要4位。我们给指针多加1位,变成5位。这样指针范围就是0~31。判断空的时候,看低4位是否相等,且高位也相等。判断满的时候,看低4位是否相等,但高位相反。
我画个表格你就明白了:
| 状态 | 判断条件 | 举例(深度16) |
|---|---|---|
| 空 | wr_ptr == rd_ptr | wr_ptr=5'b00000, rd_ptr=5'b00000 |
| 满 | wr_ptr[4] != rd_ptr[4] 且 低4位相等 | wr_ptr=5'b10000, rd_ptr=5'b00000 |
代码实现是这样的:
assign empty = (wr_ptr == rd_ptr);
assign full = (wr_ptr[4] != rd_ptr[4]) &&
(wr_ptr[3:0] == rd_ptr[3:0]);
我的小技巧:我个人习惯把指针位宽定义为 $clog2(DEPTH) + 1。这样不管深度怎么变,代码都能自动适配。省心。
3.4 避坑指南——我曾经踩过的雷
讲几个我实际项目中遇到的坑,你以后碰到了心里有数。
坑一:空满标志的时序问题
我曾经做过一个项目,FIFO深度只有4,但数据速率很高。仿真时发现空标志总是晚一个周期才拉高。查了半天,原来是空标志组合逻辑输出,但读指针更新是在时钟沿之后。说白了,读指针变了,但空标志还没反应过来。解决方案很简单:把空满标志做成寄存器输出,打一拍。
坑二:读写使能同时有效时的边界情况
当FIFO只剩最后一个数据时,如果读使能和写使能同时有效,会发生什么?读走一个,写进一个,FIFO深度不变。但如果你没处理好,空标志可能会瞬间拉高又拉低。我建议你在判断空满时,把读写使能也考虑进去:
// 更安全的空判断
assign empty_next = (wr_ptr == rd_ptr + 1) && rd_en && !wr_en;
坑三:复位后的初始状态
复位后,读写指针都归零,FIFO应该是空的。但有些设计会把读指针初始化为0,写指针初始化为-1(全1)。这样复位后FIFO就是满的。我个人不建议这么做,容易把人绕晕。老老实实都归零,然后靠空标志来判断。
警告:千万不要在组合逻辑里直接比较读写指针来产生空满标志!组合逻辑的毛刺会直接导致误判。一定要用寄存器打一拍,或者用格雷码(虽然同步FIFO不需要格雷码,但寄存器输出是必须的)。
3.5 实际项目中的经验总结
说了这么多,我总结几条实战经验:
- 深度选择:FIFO深度不是越大越好。深度太大,延迟会增加。我一般根据读写速率差和最大突发长度来算,留20%余量就够了。
- 指针位宽:用
$clog2(DEPTH) + 1自动计算,别手算。手算出错的概率太高了。 - 空满标志:一定要寄存器输出,别偷懒用组合逻辑。我曾经因为这个被折腾了一周。
- 仿真验证:写testbench时,重点测边界情况——空时读、满时写、同时读写、背靠背读写。这些场景最容易暴露问题。
好了,同步FIFO的核心内容就这些。说白了,架构不复杂,但细节决定成败。你只要把指针扩展、空满判断、寄存器输出这三个点吃透了,写出来的FIFO基本不会出大问题。下一节咱们聊异步FIFO,那才是真正的硬骨头。