1. 同步FIFO设计基础与挑战
同步FIFO(First In First Out)是数字电路设计中常用的数据缓冲结构,它在同一个时钟域内实现数据的顺序存储和读取。传统设计中,FIFO的深度通常选择2的幂次方(如8、16、32等),这样可以利用二进制计数的自然循环特性简化指针设计。但当我们需要设计深度为5、6、10等非2次幂的FIFO时,问题就变得复杂了。
我曾在实际项目中遇到过这样的需求:一个图像处理模块需要深度为6的FIFO来缓冲扫描线数据。最初尝试直接修改传统设计,结果发现空满判断总是出错。经过多次调试才发现,问题的核心在于指针循环机制。在2次幂深度FIFO中,指针从最大值跳回0时,所有地址位同时翻转,这种特性与格雷码完美配合。而非2次幂情况下,这种对称性被打破了。
格雷码作为一种循环码,其核心特性是相邻两个数之间只有一位二进制位不同。这个特性在FIFO设计中至关重要,因为它可以避免指针变化时多位同时跳变导致的亚稳态问题。但格雷码的传统应用都基于2次幂的循环空间,我们需要找到一种方法,使其适应任意深度的场景。
2. 格雷码的轴对称特性解析
2.1 格雷码的数学特性
格雷码有一个鲜为人知但极其有用的特性:轴对称性。简单来说,在2^n个格雷码序列中,序列的前半部分和后半部分呈现镜像对称关系。例如3位格雷码序列:000、001、011、010、110、111、101、100。如果我们把序列从中间分开,后半部分就是前半部分的镜像,只是最高位取反。
这个特性在传统异步FIFO设计中已经被巧妙利用。通过增加一个最高位作为"翻转标志",当指针从最大值回到0时,最高位取反,其他位保持格雷码特性。这样,即使读写指针的二进制值相同,通过最高位的不同也能区分空和满状态。
2.2 非2次幂深度的适配挑战
当我们面对深度为6的FIFO时,直接套用上述方法会遇到问题。因为6不是2的幂次方,指针的循环范围不再是自然的2^n序列。假设我们使用4位地址(可表示16个位置),我们需要让指针在特定的12个位置(2×6)间循环,比如2-7和8-13这两个区间。
这里的关键发现是:虽然总循环长度不是2^n,但我们可以将循环分成两个对称的部分,每个部分的长度等于FIFO深度。通过精心选择起始和结束地址,仍然可以保持格雷码的轴对称特性。例如深度6的情况,选择2-7和8-13这两个区间,它们在二进制表示上具有对称性:
2: 0010 7: 0111 8: 1000 13: 11013. 非2次幂深度FIFO的指针设计
3.1 起始和结束地址的计算
让我们以深度6的FIFO为例,详细说明指针的初始化过程。我们需要两个关键参数:
DEPTH = 6:FIFO的实际深度ADDR = 3:地址位宽,因为$clog2(6)=3$
起始地址(start_count)和结束地址(end_count)的计算公式如下:
start_count = {1'b1,{ADDR{1'b0}}} - DEPTH; // 8-6=2 end_count = {1'b0,{ADDR{1'b1}}} + DEPTH; // 7+6=13这个计算确保了:
- 循环范围覆盖2×DEPTH个地址(这里是12个)
- 将循环范围对称地分为两部分(2-7和8-13)
- 两部分在最高位互为补码,保持了格雷码的对称性
对于深度5的FIFO,计算方式类似:
start_count = 8-5=3 end_count = 7+5=123.2 指针跳转逻辑的实现
指针的跳转逻辑需要处理三种情况:
- 正常递增:指针在区间内逐个地址递增
- 区间跳转:到达结束地址时跳回起始地址
- 复位状态:指针复位到起始地址
以下是写指针的Verilog实现:
always@(posedge clk or negedge rst_n) begin if(!rst_n) begin write_ptr <= start_count; end else if(winc && !wfull) begin if(write_ptr == end_count) write_ptr <= start_count; else write_ptr <= write_ptr + 1; end else begin write_ptr <= write_ptr; end end读指针的实现逻辑完全相同,只是信号名称不同。这种设计确保了指针在超出预设范围时能够正确循环,同时保持递增的连续性。
4. 格雷码转换与空满判断
4.1 二进制到格雷码的转换
由于我们的指针循环范围不是从0开始的连续序列,直接应用标准格雷码转换会导致问题。我们需要先将指针值"归一化"到从0开始的连续范围,再进行格雷码转换。
assign real_write_ptr = write_ptr[ADDR] ? write_ptr : (write_ptr - start_count); assign real_read_ptr = read_ptr[ADDR] ? read_ptr : (read_ptr - start_count); assign gray_wr_ptr = real_write_ptr ^ (real_write_ptr >> 1); assign gray_rd_ptr = real_read_ptr ^ (real_read_ptr >> 1);这里的关键技巧是:
- 对于最高位为1的指针值(8-13区间),保持原值
- 对于最高位为0的指针值(2-7区间),减去起始地址(2)使其从0开始
- 然后应用标准的格雷码转换公式
4.2 空满状态的判断逻辑
空满判断基于转换后的格雷码比较。与传统设计类似,但需要考虑我们的特殊指针范围:
assign full_o = (gray_wr_ptr == {~gray_rd_ptr[ADDR:ADDR-1], gray_rd_ptr[ADDR-2:0]}) ? 1'b1 : 1'b0; assign empty_o = (gray_rd_ptr == gray_wr_ptr) ? 1'b1 : 1'b0;空状态判断很简单:读写指针的格雷码完全相同时,FIFO为空。满状态的判断稍微复杂些:需要最高位和次高位相反,其余位相同。这与传统设计一致,但由于我们前期的指针处理,这个逻辑在非2次幂深度下同样有效。
5. 实际应用与性能考量
5.1 存储器的实现
FIFO的存储器部分使用标准的双端口RAM实现,但需要注意地址索引的使用。由于我们进行了指针归一化处理,实际存储地址应该使用归一化后的低几位:
parameter WIDTH = 8; // 数据位宽 reg [WIDTH-1:0] ram_mem [0:DEPTH-1]; always@(posedge wclk) begin if(wenc) ram_mem[real_write_ptr[ADDR-1:0]] <= wdata; end always@(posedge rclk) begin if(renc) rdata <= ram_mem[real_read_ptr[ADDR-1:0]]; end这里real_write_ptr[ADDR-1:0]提取归一化指针的低几位,正好对应0-5的存储地址范围。
5.2 时序与面积优化
在实际应用中,这种设计可能会比标准2次幂深度FIFO消耗更多的逻辑资源,主要体现在:
- 额外的地址比较逻辑(判断是否到达end_count)
- 指针归一化处理的加减法运算
- 更复杂的空满判断逻辑
为了优化时序,可以考虑:
- 将关键路径(如空满信号生成)进行流水线处理
- 使用独热码编码状态机来简化控制逻辑
- 在高速场景下,可以牺牲少量存储空间,将FIFO深度补齐到最近的2次幂
我在一个实际项目中测量过,深度6的FIFO采用这种设计比深度8的标准设计节省了约25%的存储空间,但增加了约15%的逻辑资源。在存储资源紧张但逻辑资源充裕的场景下,这种折中是值得的。
6. 验证与调试技巧
验证非2次幂深度FIFO时,需要特别关注边界条件。我通常会构造以下测试场景:
- 连续写入直到FIFO满,然后连续读出直到空
- 交替进行单次写入和单次读取
- 在FIFO接近满时进行随机读写操作
- 特别测试指针从end_count跳回start_count的情况
在调试过程中,有几个常见问题需要注意:
- 指针归一化处理不正确,导致地址越界
- 空满判断逻辑没有考虑非2次幂的特殊性
- 复位后指针没有正确初始化为start_count
一个实用的调试技巧是将指针值、格雷码值以及空满信号都引出到顶层,方便用逻辑分析仪观察。在FPGA原型验证时,可以设置触发条件捕获异常状态。