3. FIFO设计基础:同步FIFO原理、异步FIFO原理、格雷码指针设计

好,咱们今天聊聊FIFO。FIFO这东西,说白了就是个先进先出的数据缓冲池。做控制器设计,尤其是跨时钟域或者数据流整形的时候,FIFO几乎是绕不开的标配。我个人习惯把FIFO分成两大类:同步FIFO和异步FIFO。别看名字就差两个字,背后的设计思路和坑,差别可大了去了。

3.1 同步FIFO原理

同步FIFO,就是读写时钟是同一个。你想想看,读写都在同一个节拍下工作,那控制逻辑就简单多了。核心就三个东西:存储体(通常是双端口RAM)、写指针、读指针。

写指针指向下一个要写入的地址,每写一次就加1。读指针指向下一个要读出的地址,每读一次也加1。当两个指针相等时,要么是空,要么是满。怎么区分?我教你一个土办法:多加一个bit位。

核心判断逻辑:

  • 空标志:读写指针的所有位都相等
  • 满标志:读写指针的高位相反,低位相等

举个例子,深度为4的FIFO,指针需要3位(2位地址+1位扩展位)。写指针跑到4'b1000,读指针还在4'b0000,高位不同低位相同——满。写指针追上读指针,都是4'b0000——空。嗯,这里要注意,同步FIFO的满空判断是组合逻辑直接比较,延迟很小,适合高频场景。

我在项目中遇到过一个问题:同步FIFO的深度选多大?有人觉得越大越好,其实不是。深度太大,面积和功耗都上去了,而且读延迟也会变长。我一般建议,深度取最大可能突发长度的1.5倍到2倍,够用就行。

3.2 异步FIFO原理

异步FIFO就麻烦多了。读写时钟不同,甚至频率都不一样。这时候最怕什么?亚稳态。你想想看,写指针从写时钟域传到读时钟域,如果直接传,采样的时候刚好在跳变沿上,读到的值可能既不是0也不是1,整个逻辑就崩了。

怎么解决?两个关键点:

  1. 格雷码编码指针:每次只变化1位,降低多bit同步时的出错概率
  2. 两级同步器:用两个触发器打两拍,把亚稳态概率降到可接受范围

异步FIFO的空满判断也比同步复杂。因为指针同步过来有延迟,所以空满标志不能实时反映真实状态。我习惯的做法是:

  • 空标志:用同步后的读指针和写指针比较(保守一点,宁可显示空,也不能读空)
  • 满标志:用同步后的写指针和读指针比较(宁可显示满,也不能写满)

我曾经踩过的坑:异步FIFO的深度必须是2的幂次方。为什么?因为格雷码的循环特性要求地址空间是2^n。如果你非要搞个深度6的FIFO,格雷码会跳变多位,同步就失效了。别问我怎么知道的,问就是吃过亏。

3.3 格雷码指针设计

格雷码这东西,说白了就是相邻两个数之间只有1位不同。二进制转格雷码的公式很简单:gray = bin ^ (bin >> 1)。反过来,格雷码转二进制稍微麻烦点,需要逐位异或。

我一般把格雷码指针设计成两个模块:

  • 指针生成模块:在本地时钟域里,二进制指针加1,然后转成格雷码
  • 指针同步模块:把格雷码打两拍传到另一个时钟域,再转回二进制用于比较

这里有个细节:格雷码同步后转二进制,这个转换逻辑是在接收时钟域完成的。为什么不在发送端转好再同步?因为二进制多bit同时变化,同步风险太大。格雷码天然适合跨时钟域传输。

我的个人习惯:写指针和读指针都存成格雷码形式,只在需要比较时才转二进制。这样既省了转换逻辑,又保持了同步的安全性。不过要注意,格雷码比较空满时,需要把格雷码转成二进制再比较,不能直接拿格雷码比大小——格雷码没有单调性。

3.4 知识体系结构图

下面这张图,是我自己总结的FIFO设计知识脉络。你看一眼,心里就有谱了。

FIFO设计知识体系 FIFO设计基础 同步FIFO 异步FIFO 格雷码指针 双端口RAM 读写指针比较 空满标志生成 两级同步器 亚稳态处理 深度为2^n 二进制→格雷码 格雷码→二进制 跨时钟域传输 核心:指针设计 + 跨时钟域同步 + 空满判断

3.5 代码示例:格雷码指针核心逻辑

光说不练假把式。我贴一段我自己常用的格雷码指针代码,你感受一下。

// 二进制转格雷码
assign gray_ptr = bin_ptr ^ (bin_ptr >> 1);

// 格雷码转二进制(组合逻辑)
always @(*) begin
  bin_from_gray[WIDTH-1] = gray_in[WIDTH-1];
  for (int i = WIDTH-2; i >= 0; i--) begin
    bin_from_gray[i] = bin_from_gray[i+1] ^ gray_in[i];
  end
end

// 两级同步器(跨时钟域)
always @(posedge clk_dst or negedge rst_n) begin
  if (!rst_n) begin
    sync_reg1 <= 0;
    sync_reg2 <= 0;
  end else begin
    sync_reg1 <= gray_ptr_src;
    sync_reg2 <= sync_reg1;
  end
end

一个小技巧:格雷码指针的位宽要比地址位多1位。比如深度16的FIFO,地址需要4位,指针就要5位。多出来的高位用来区分空满,前面讲过了。这个细节,很多新手会忽略。

好了,FIFO设计的基础就这些。同步FIFO重在简单直接,异步FIFO重在跨时钟域安全,格雷码指针是连接两者的桥梁。你把这些搞明白了,后面做控制器低延迟读写通道设计,心里就有底了。


公众号:蓝海资料掘金营,微信deep3321