FIFO(First In First Out,先进先出队列)是数字芯片里最常用的存储缓冲结构:数据从写口按顺序写入、从读口按同样顺序读出,读写两侧各有一套控制逻辑,中间是一片存储阵列。它像一个蓄水池 —— 上游往里灌水(写)、下游往外抽水(读),两边速率可以不同,池子负责「削峰填谷」。工程上 FIFO 有三大用途:
NPU 里的真实例子:卷积计算时,feature map 从 DDR/NoC 读进来的速度和 PE 阵列(MaxPool/MAC 阵列)消费的速度不一致;滑窗每次要凑齐 K×K 窗口数据才开始算,窗口间的行重叠(比如 3×3 卷积 stride 1 要缓存上两行)也常用 line buffer(本质是按行组织的 FIFO/移位链) 实现。NPU 的数据通路里,FIFO 出现的密度非常高 —— 所以数字前端面试几乎必考。
FIFO 的基本规矩只有两条:空(empty)时不读(读会拿到无效/重复数据,underflow),满(full)时不写(写会覆盖还没读走的数据,overflow)。判断空满靠两个指针:
指针其实是「一直加 1 的计数器」,回绕(wrap-around)后自然循环访问存储阵列 —— 所以指针位宽通常比地址多 1 位,用来区分「空」和「满」两种「指针相等」的状态(见 2.3 节)。同步 FIFO 的读写用同一个时钟;异步 FIFO 读写各用一个时钟,那是本页的重头戏。
用一个额外的 count 计数器实时记录 FIFO 里的数据个数:写有效且不满则 +1,读有效且不空则 −1。空满判断最直观:
module sync_fifo #(
parameter DEPTH = 8, // FIFO 深度(建议取 2 的幂)
parameter DW = 8 // 数据位宽
)(
input wire clk, // 读写共用时钟(同步 FIFO 的"同")
input wire rst_n, // 低有效复位
input wire wr_en, // 写使能
input wire [DW-1:0] wr_data, // 写数据
input wire rd_en, // 读使能
output reg [DW-1:0] rd_data, // 读数据(时序逻辑输出)
output wire full, // 满标志
output wire empty // 空标志
);
localparam AW = $clog2(DEPTH); // 地址位宽:DEPTH=8 时 AW=3
reg [DW-1:0] mem [0:DEPTH-1]; // ① 存储阵列:DEPTH 个 DW 位寄存器
reg [AW:0] wptr; // ② 写指针(比地址多 1 位,见 2.3)
reg [AW:0] rptr; // ③ 读指针
reg [AW:0] count; // ④ 当前数据个数(0~DEPTH)
assign full = (count == DEPTH); // ⑤ 满:个数达到深度
assign empty = (count == 0); // ⑥ 空:个数为 0
/* ⑦ 写指针更新:满不写 */
always @(posedge clk or negedge rst_n) begin
if (!rst_n) wptr <= {AW+1{1'b0}};
else if (wr_en && !full) wptr <= wptr + 1'b1; // 回绕循环
end
/* ⑧ 写数据:用写指针低 AW 位当 RAM 地址 */
always @(posedge clk) begin
if (wr_en && !full)
mem[wptr[AW-1:0]] <= wr_data;
end
/* ⑨ 读指针更新:空不读 */
always @(posedge clk or negedge rst_n) begin
if (!rst_n) rptr <= {AW+1{1'b0}};
else if (rd_en && !empty) rptr <= rptr + 1'b1;
end
/* ⑩ 读数据:寄存器输出,延迟一拍(FW=1 的同步读) */
always @(posedge clk or negedge rst_n) begin
if (!rst_n) rd_data <= {DW{1'b0}};
else if (rd_en && !empty) rd_data <= mem[rptr[AW-1:0]];
end
/* ⑪ 计数器:同拍又读又写时个数不变 */
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= {AW+1{1'b0}};
else begin
case ({wr_en && !full, rd_en && !empty})
2'b10: count <= count + 1'b1; // 只写 +1
2'b01: count <= count - 1'b1; // 只读 -1
2'b11: count <= count; // 读写相抵
default: count <= count; // 都不动
endcase
end
end
endmodule
逐行关键点讲解:
reg [DW-1:0] mem [0:DEPTH-1] 是 DEPTH 个 DW 位寄存器,FPGA 综合成分布式 RAM/BRAM,ASIC 综合成寄存器堆或双口 SRAM。!full、读操作必须带 !empty —— 这是 FIFO 的「满不写、空不读」铁律;指针本身只加不减,回绕后自然循环。计数器法的代价是 count 这个 AW+1 位加/减法器,位宽大时面积和时序都不划算。更优雅的经典做法:指针多留 1 位最高位,每次回绕时把它取反。于是:
为什么成立?DEPTH=8 时指针 4 位(0000~1111+回绕)。未回绕时两个指针相同 → 空;写指针恰好比读指针多转一圈时,只有最高位相反、低 3 位相同 → 满。两种「低 3 位相等」的情形被最高位干净地区分开 —— 这一位就是「圈数奇偶性」。
// 法二核心:只改判断,其余与法一相同(删去 count)
wire rem = (wptr == rptr); // 空:完全相等
wire ful = (wptr[AW] != rptr[AW]) && (wptr[AW-1:0] == rptr[AW-1:0]); // 满:最高位相反 + 其余相同
| 对比项 | 法一:计数器法 | 法二:最高位拓展法 |
|---|---|---|
| 空判断 | count == 0 | wptr == rptr |
| 满判断 | count == DEPTH | 最高位相异 + 其余位相同 |
| 额外资源 | AW+1 位加减计数器 | 无(指针本就多 1 位) |
| 读写同拍 | 需专门处理 2'b11 分支 | 天然正确(两指针独立更新) |
| 可读性 / 面试 | 直观,适合先讲 | 更常见于工业代码,是异步 FIFO 格雷码判断的基础,必须掌握 |
工程与面试怎么问:
!full(或读忘了 !empty),testbench 里故意写满 / 读空必现 bug;wptr[AW]==~rptr[AW] 之外还要求比较所有位取反 —— 只有最高位相反,其余位必须相同。
FIFO 深度 = 「突发期间最多会积压多少数据」。设:写侧每个写周期写 N 个数据,读侧每个读周期读 M 个数据,一次突发共写入 B 个数据,写时钟频率 fw、读时钟频率 fr。推导只需三步:
两个极端自检:读比写慢(fr·M/fw·N 越小)→ 深度趋近 B,整包都得存;读比写快 → 公式出现负数,说明瓶颈不在 FIFO,理论深度为 0。另外注意平均速率相等 ≠ 深度为 0:突发是「先集中灌、再慢慢抽」,峰值积压必须由深度兜住。
例题 1(基础直套公式):写时钟 200MHz 连续写入,读时钟 100MHz,读写侧每拍各处理 1 个数据,突发长度 1000。求最小 FIFO 深度。
写入 B = 1000,N = M = 1,f_w = 200MHz,f_r = 100MHz
读走 = 1000 × (100/200) = 500
D_min = 1000 − 500 = 500 → 工程上向上取 512(2 的幂,给格雷码方案留余地)
例题 2(经典 24 深):写时钟 100MHz,读时钟 80MHz,一次突发写入 120 个数据,两侧每拍各 1 个。
读走 = 120 × (80/100) = 96
D_min = 120 − 96 = 24
例题 3(带 idle 的真实突发):写时钟 100MHz 连续写 1000 个(耗时 10μs);读时钟 80MHz,但读使能每 4 拍只有效 3 拍(平均读速率 80M×3/4 = 60M/s)。
突发期间读走 = 10μs × 60M = 600
D_min = 1000 − 600 = 400 → 取 512
工程与面试怎么问:「给你两个时钟频率和突发长度,算 FIFO 最小深度」—— 就是 3.2 的套路;追问「为什么实际深度要大于计算值」—— 答:公式假设读写严格连续,没有算控制握手开销、同步器延迟(异步 FIFO 见第 8 节)和工程裕量,实际按 2 的幂向上取并留 20% 裕量。
亚稳态(Metastability)指触发器在时钟有效沿采样到「正在变化」的数据时,输出既不是稳定的 0 也不是稳定的 1,而是停留在中间电平、或在返回合法电平前不规则振荡,并且经过多长时间、最终停在 0 还是 1 都不确定。物理成因:触发器内部由背靠背反相器构成双稳态存储环,像「立在碗底的球」;只有在时钟沿附近的 setup/hold 窗口内数据保持稳定,球才能落进 0 或 1 两个稳态之一。异步信号(来自另一个时钟域)随时可能变化,一旦翻转事件落进这个窗口,球就被踢到碗壁上 —— 滚多久、滚向哪边,纯看物理运气。
三个必须记住的性质:
亚稳态的危害用 MTBF(Mean Time Between Failures,平均无故障时间) 量化:
MTBF = \dfrac{e^{\,t_{MET}/\tau}}{W \cdot f_{clk} \cdot f_{data}}公式的灵魂在指数项:恢复时间每多一个时钟周期,MTBF 乘上 eTclk/τ,呈指数级增长。假设 500MHz 系统、τ ≈ 50ps,不打任何恢复时间直接用亚稳输出,MTBF 可能只有纳秒级(必挂);让它多等一拍再使用,MTBF 可达秒级;等两拍,轻松达到年/百年量级 —— 这就是「打两拍」的数学依据。
三个典型翻车现场:
面试怎么问:「什么是亚稳态?」「亚稳态能被消除吗?」(不能,只能降概率)「MTBF 公式里各项是什么?为什么打两拍能极大提升 MTBF?」「亚稳态的输出如果进了组合逻辑会怎样?」
// 两级同步器:每个跨域单 bit 信号一套
module sync2 (
input wire clk_dst, // 目的时钟域
input wire rst_n,
input wire d_async, // 异步输入(来自别的时钟域/引脚)
output wire q_sync // 同步后的信号
);
reg q1, q2;
always @(posedge clk_dst or negedge rst_n) begin
if (!rst_n) {q2, q1} <= 2'b00;
else {q2, q1} <= {q1, d_async}; // 打两拍
end
assign q_sync = q2;
endmodule
面试怎么问:「两级同步器的原理?打三拍行不行,代价是什么?」「为什么不能把 32 位数据总线直接打两拍?」「为什么跨域信号前要先寄存一级?」—— 全部对应上面三条。
格雷码(Gray Code)是一种相邻两个码字之间只有 1 位不同的二进制编码(单位距离码)。转换公式只有两条,必须背下来:
G = B \oplus (B \gg 1) \qquad\qquad B_i = G_i \oplus B_{i+1}\ (\text{自高位向低位恢复})| 十进制 | 二进制 B | 格雷码 G | 相对上一行变化位数 |
|---|---|---|---|
| 0 | 0000 | 0000 | — |
| 1 | 0001 | 0001 | 1 位(LSB) |
| 2 | 0010 | 0011 | 1 位 |
| 3 | 0011 | 0010 | 1 位 |
| 4 | 0100 | 0110 | 1 位 |
| 5 | 0101 | 0111 | 1 位 |
| 6 | 0110 | 0101 | 1 位 |
| 7 | 0111 | 0100 | 1 位 |
| 8 | 1000 | 1100 | 1 位(回绕到 0 也只变 1 位) |
| … | … | … | 恒为 1 位 |
对照二进制:0111(7)→ 1000(8)四位同时翻转;格雷码 0100 → 1100 只翻最高位。注意 15 → 0 的回绕(1111 → 0000,格雷 1000 → 0000)同样只变 1 位 —— 这保证了 FIFO 指针循环计数时性质不破。
// 二进制 → 格雷(纯组合,一行)
function [AW:0] bin2gray(input [AW:0] b);
bin2gray = (b >> 1) ^ b;
endfunction
// 格雷 → 二进制(自高位向低位累异或)
function [AW:0] gray2bin(input [AW:0] g);
integer i;
begin
for (i = AW; i >= 0; i = i - 1)
gray2bin[i] = ^(g >> i); // 高位前缀异或:B[i] = G[i]^G[i+1]^...^G[AW]
end
endfunction
异步 FIFO 中,写指针要在写时钟域产生、读时钟域比较(判空),读指针同理(判满)—— 多位指针必须跨域。若用二进制指针直采:4 位指针 0111→1000 四位同时翻转,恰逢采样沿时各位恢复结果不一致,可能采出 0111~1000 之外的非法中间值(如 1111),空满判断直接错乱。换成格雷码指针再跨域:
面试怎么问:「异步 FIFO 为什么用格雷码?」「格雷码怎么转换,写个函数」「用格雷码后空满判断怎么做的?」「为什么指针必须同步到对方域而不是把数据同步过去?」(数据量太大逐个同步不现实,只同步指针,FIFO 存储阵列本身是双口共享的)。
要点:① 存储阵列必须是双口 RAM(写口时钟与读口时钟独立,普通单口寄存器组做不到);② 跨域的只有指针,且指针编码成格雷码;③ 每个指针的每一位都经过同一套两级同步器;④ empty 只能在读域算(它需要最新的读指针),wfull 只能在写域算。
module async_fifo #(
parameter DEPTH = 8, // 深度必须为 2 的幂(格雷码回绕要求)
parameter DW = 8
)(
// 写时钟域
input wire wclk,
input wire wrst_n,
input wire winc, // 写使能
input wire [DW-1:0] wdata,
output wire wfull,
// 读时钟域
input wire rclk,
input wire rrst_n,
input wire rinc, // 读使能
output reg [DW-1:0] rdata,
output wire rempty
);
localparam AW = $clog2(DEPTH);
reg [DW-1:0] mem [0:DEPTH-1]; // 双口 RAM
reg [AW:0] wbin, wbin_next, wgray_next, wptr; // 写指针:二进制 + 格雷
reg [AW:0] rbin, rbin_next, rgray_next, rptr; // 读指针:二进制 + 格雷
reg [AW:0] wptr_sync1, wptr_sync2; // wgray → 读域 同步器
reg [AW:0] rptr_sync1, rptr_sync2; // rgray → 写域 同步器
/* ---------- 写时钟域 ---------- */
// 指针与格雷码一起更新:{二进制, 格雷}
always @(posedge wclk or negedge wrst_n) begin
if (!wrst_n) {wbin, wptr} <= 0;
else {wbin, wptr} <= {wbin_next, wgray_next};
end
assign wbin_next = wbin + (winc & ~wfull); // 满不写
assign wgray_next = (wbin_next >> 1) ^ wbin_next; // 组合格雷码
// 满:wgray_next 与「同步进写域的 rgray」最高两位相反、其余位相同
assign wfull = (wgray_next ==
{~rptr_sync2[AW:AW-1], rptr_sync2[AW-2:0]});
// 读格雷指针 → 写域:两级同步器(FF 直连 FF,不许插组合逻辑)
always @(posedge wclk or negedge wrst_n) begin
if (!wrst_n) {rptr_sync2, rptr_sync1} <= 0;
else {rptr_sync2, rptr_sync1} <= {rptr_sync1, rptr};
end
/* ---------- 读时钟域 ---------- */
always @(posedge rclk or negedge rrst_n) begin
if (!rrst_n) {rbin, rptr} <= 0;
else {rbin, rptr} <= {rbin_next, rgray_next};
end
assign rbin_next = rbin + (rinc & ~rempty); // 空不读
assign rgray_next = (rbin_next >> 1) ^ rbin_next;
// 空:rgray_next 与「同步进读域的 wgray」完全相等
assign rempty = (rgray_next == wptr_sync2);
always @(posedge rclk or negedge rrst_n) begin
if (!rrst_n) {wptr_sync2, wptr_sync1} <= 0;
else {wptr_sync2, wptr_sync1} <= {wptr_sync1, wptr};
end
/* ---------- 读数据(寄存器输出,延迟一拍) ---------- */
always @(posedge rclk or negedge rrst_n) begin
if (!rrst_n) rdata <= 0;
else if (rinc && !rempty) rdata <= mem[rbin[AW-1:0]];
end
endmodule
rempty = (rgray_next == wptr_sync2) —— 同步进来的写格雷指针最多落后真实值,「以为还没写」只会误报空(保守),绝不漏报空;用 rgray_next(而非 rptr)判断是把「本拍要发生的读」也提前算进去,标志时序更保守。工程与面试怎么问:「画出异步 FIFO 框图」(按图 3 讲四要素:双口 RAM / 两个格雷码指针 / 两条同步链 / 空满逻辑);「空和满分别在哪个域产生,为什么?」;「指针同步用几拍,延迟对 FIFO 有什么影响?」(2 拍,标志保守但安全);「如果深度不是 2 的幂怎么办?」(非 2 幂深度要用特殊格雷码/非对称指针方案,或直接取 2 的幂)。
异步 FIFO 的深度计算在 3.1 公式基础上,还要叠加跨域特有的考虑:
例题:写时钟 100MHz,连续突发写入 512 个数据;读时钟 66MHz,每拍读出 1 个。求异步 FIFO 最小深度。
① 突发时间 = 512 / 100MHz = 5.12 μs
② 期间读走 = 5.12 μs × 66MHz = 337.92 → 337 个
③ 公式深度 = 512 − 337.92 ≈ 174.08 → 175
④ 格雷码方案 → 向上取 2 的幂 = 256
⑤ 再核对:同步器 2~3 拍延迟期间多积压 ≈ 3 × 66MHz⁻¹ 内的写入量(约 3 个),裕量充分 ✔
变式(读写平均速率相同的错峰突发):写域 100MHz、读域 100MHz,写侧每 10 拍突发 8 拍(平均 80M/s),读侧连续读(100M/s)。由于读快于写,峰值积压出现在「8 拍写入窗口」内:8 写 − 窗口内读走 8×(100/100)×(8/10)=6.4 → 峰值积压 1.6 → 深度取 2~4 即可,但工程上仍常给到 8/16 以吸收抖动。
面试怎么问:「异步 FIFO 深度和同步 FIFO 深度算法有什么区别?」(同步延迟 + 2 的幂约束 + 最坏突发假设);「读比写快还需要 FIFO 吗?」(数据到达本身就是突发的仍需要,深度按峰值积压算);「为什么深度取 2 的幂?」(格雷码回绕保持单位距离)。
电平同步:跨域信号是「会保持一段时间的电平」(如中断、忙标志),直接过两级同步器(第 5 节)。约束:电平必须保持至少 2~3 个目的域时钟周期,否则会被漏采。
脉冲同步(toggle 同步):要传的是「单拍脉冲」(比目的域周期还窄,或相位随机直接同步会丢),经典做法是把脉冲转成电平翻转再跨域:
// 发送域:每个脉冲让 toggle 电平翻转一次
always @(posedge clk_src or negedge rst_n)
if (!rst_n) toggle <= 1'b0;
else if (pulse) toggle <= ~toggle;
// 接收域:toggle 打两拍后,与上一拍异或还原出单拍脉冲
always @(posedge clk_dst or negedge rst_n)
if (!rst_n) {t_sync2, t_sync1} <= 2'b00;
else {t_sync2, t_sync1} <= {t_sync1, toggle};
assign pulse_dst = t_sync2 ^ t_sync3; // t_sync3 是再打一拍的寄存器
原理:电平翻转一定会被目的域采到(电平保持时间足够长);接收域比较「翻转前后的电平」重建出脉冲。局限:连续两个脉冲间隔小于 2 个目的域周期会丢脉冲,只适合低频事件。
多 bit 数据的两种正统方案:
握手(4-phase / 电平握手)是低速率多 bit 数据跨域最常考的方案:数据本身「放稳不动」,用两个单 bit 信号(req / ack)来回打两拍,完成一次「确认送达」。
五步时序口诀:数据放稳 → req 高 → ack 高(对方采样)→ req 低 → ack 低。代价:每传一次数据要 4~5 个时钟拍,吞吐很低,所以只用于配置寄存器、状态回馈等低频场景;高频流式数据请用异步 FIFO。
| 跨域对象 | 推荐方案 | 一句话理由 |
|---|---|---|
| 单 bit 电平(慢变化) | 两级同步器 | 最简单,MTBF 足够 |
| 单 bit 脉冲(窄/随机相位) | toggle 同步 | 把「难采的脉冲」变成「好采的电平」 |
| 多 bit 数据(低速偶发) | 握手 req-ack / DMUX | 数据静止 + 单 bit 控制跨域 |
| 多 bit 数据(高速连续流) | 异步 FIFO | 指针编码跨域,吞吐最高 |
| 多 bit 计数值(单调递增) | 格雷码编码 | 相邻只变 1 位,天然防撕裂 |
面试怎么问:「除了两级同步器和异步 FIFO,还知道哪些 CDC 手段?」「握手协议完整流程画一下,每一拍在做什么?」「toggle 同步为什么会丢脉冲?」「数据为什么要『静止』,静止多久才安全?」。
// 复位同步器:异步复位、同步释放(每个时钟域一份)
module reset_sync (
input wire clk,
input wire rst_n_async, // 外部/全局异步复位
output wire rst_n_sync // 本域同步释放的复位
);
reg rst_ff1, rst_ff2;
always @(posedge clk or negedge rst_n_async) begin
if (!rst_n_async) {rst_ff2, rst_ff1} <= 2'b00; // 复位立即异步生效
else {rst_ff2, rst_ff1} <= {rst_ff1, 1'b1}; // 释放同步打两拍
end
assign rst_n_sync = rst_ff2;
endmodule
以下资源均经人工访问核实(2026-09),⭐ 为强烈推荐。