1. 引言
在数字系统设计中,跨时钟域(CDC,Clock Domain Crossing)是 FPGA 和 ASIC 开发中最容易出问题的环节之一。当两个时钟域之间需要传递数据时,如果直接使用寄存器打拍同步,只能解决单比特信号的亚稳态问题;对于多比特数据总线,则需要借助异步 FIFO 来安全地完成数据缓冲与传递。
异步 FIFO 的核心难点有三个:
- 读写指针的跨时钟域同步:直接同步二进制计数器会产生多位同时翻转,导致采样到错误值;
- 空满状态的准确判断:空满标志必须在各自时钟域内实时、正确地产生;
- 亚稳态的消除:同步器只能降低亚稳态概率,不能完全消除,需要配合合理的时序约束。
本文将以一段完整可综合的 Verilog 异步 FIFO 代码为例,逐模块拆解其设计思路,并补充仿真验证与工程实践建议。
2. 异步 FIFO 的基本结构
一个典型的异步 FIFO 由以下部分组成:
- 双端口 RAM:用于存储数据,写端口在写时钟域,读端口在读时钟域;
- 写指针与读指针:分别由写时钟和读时钟驱动,指向当前读写地址;
- 格雷码转换:将二进制指针转换为格雷码,保证跨时钟域同步时每次只有一位变化;
- 两级同步器:将对方时钟域的格雷码指针打两拍,消除亚稳态;
- 空满判断逻辑:基于同步后的指针计算空满标志。
整体结构如下图所示:
#mermaid-svg-DyJCr2Ullk8BSmBj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DyJCr2Ullk8BSmBj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DyJCr2Ullk8BSmBj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DyJCr2Ullk8BSmBj .error-icon{fill:#552222;}#mermaid-svg-DyJCr2Ullk8BSmBj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DyJCr2Ullk8BSmBj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DyJCr2Ullk8BSmBj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DyJCr2Ullk8BSmBj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DyJCr2Ullk8BSmBj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DyJCr2Ullk8BSmBj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DyJCr2Ullk8BSmBj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DyJCr2Ullk8BSmBj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DyJCr2Ullk8BSmBj .marker.cross{stroke:#333333;}#mermaid-svg-DyJCr2Ullk8BSmBj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DyJCr2Ullk8BSmBj p{margin:0;}#mermaid-svg-DyJCr2Ullk8BSmBj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DyJCr2Ullk8BSmBj .cluster-label text{fill:#333;}#mermaid-svg-DyJCr2Ullk8BSmBj .cluster-label span{color:#333;}#mermaid-svg-DyJCr2Ullk8BSmBj .cluster-label span p{background-color:transparent;}#mermaid-svg-DyJCr2Ullk8BSmBj .label text,#mermaid-svg-DyJCr2Ullk8BSmBj span{fill:#333;color:#333;}#mermaid-svg-DyJCr2Ullk8BSmBj .node rect,#mermaid-svg-DyJCr2Ullk8BSmBj .node circle,#mermaid-svg-DyJCr2Ullk8BSmBj .node ellipse,#mermaid-svg-DyJCr2Ullk8BSmBj .node polygon,#mermaid-svg-DyJCr2Ullk8BSmBj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DyJCr2Ullk8BSmBj .rough-node .label text,#mermaid-svg-DyJCr2Ullk8BSmBj .node .label text,#mermaid-svg-DyJCr2Ullk8BSmBj .image-shape .label,#mermaid-svg-DyJCr2Ullk8BSmBj .icon-shape .label{text-anchor:middle;}#mermaid-svg-DyJCr2Ullk8BSmBj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DyJCr2Ullk8BSmBj .rough-node .label,#mermaid-svg-DyJCr2Ullk8BSmBj .node .label,#mermaid-svg-DyJCr2Ullk8BSmBj .image-shape .label,#mermaid-svg-DyJCr2Ullk8BSmBj .icon-shape .label{text-align:center;}#mermaid-svg-DyJCr2Ullk8BSmBj .node.clickable{cursor:pointer;}#mermaid-svg-DyJCr2Ullk8BSmBj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DyJCr2Ullk8BSmBj .arrowheadPath{fill:#333333;}#mermaid-svg-DyJCr2Ullk8BSmBj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DyJCr2Ullk8BSmBj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DyJCr2Ullk8BSmBj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DyJCr2Ullk8BSmBj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DyJCr2Ullk8BSmBj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DyJCr2Ullk8BSmBj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DyJCr2Ullk8BSmBj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DyJCr2Ullk8BSmBj .cluster text{fill:#333;}#mermaid-svg-DyJCr2Ullk8BSmBj .cluster span{color:#333;}#mermaid-svg-DyJCr2Ullk8BSmBj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DyJCr2Ullk8BSmBj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DyJCr2Ullk8BSmBj rect.text{fill:none;stroke-width:0;}#mermaid-svg-DyJCr2Ullk8BSmBj .icon-shape,#mermaid-svg-DyJCr2Ullk8BSmBj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DyJCr2Ullk8BSmBj .icon-shape p,#mermaid-svg-DyJCr2Ullk8BSmBj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DyJCr2Ullk8BSmBj .icon-shape .label rect,#mermaid-svg-DyJCr2Ullk8BSmBj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DyJCr2Ullk8BSmBj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DyJCr2Ullk8BSmBj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DyJCr2Ullk8BSmBj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 读时钟域
写时钟域
写指针(二进制)
写格雷码
读格雷码同步器
满标志
读指针(二进制)
读格雷码
写格雷码同步器
空标志
双端口 RAM
3. 为什么使用格雷码指针
二进制计数器在递增时,多位可能同时翻转。例如从 0111 变为 1000,四位全部变化。如果此时恰好被另一个时钟域采样,同步器可能采到 0000、1111 等任意中间值,导致指针严重错误。
格雷码(Gray Code)的特点是相邻两个值之间只有一位不同。即使采样发生在翻转瞬间,同步器最多采到旧值或新值,绝不会采到错误的中间状态。因此,跨时钟域同步的指针必须使用格雷码。
二进制转格雷码的公式为:
verilog
gray = (bin >> 1) ^ bin;
对应代码中的实现:
verilog
assign wgray_next = (wbin_next >> 1) ^ wbin_next;
assign rgray_next = (rbin_next >> 1) ^ rbin_next;
4. 空满判断的核心技巧
4.1 指针位宽多一位
为了区分「空」和「满」两种状态,指针位宽比地址位宽多一位。当读写指针完全相等时为空;当写指针追上读指针(最高位不同、其余位相同)时为满。
以深度为 4 的 FIFO 为例,地址位宽为 2,指针位宽为 3:
| 状态 | 写指针 | 读指针 | 说明 |
|---|---|---|---|
| 空 | 000 | 000 | 完全相等 |
| 满 | 100 | 000 | 最高位不同,低位相同 |
4.2 格雷码下的满判断
在格雷码域中,判断满的条件是:同步后的读格雷码与写格雷码的最高两位取反后相等。代码中的掩码实现如下:
verilog
localparam [PTR_WIDTH-1:0] FULL_COMPARE_MASK =
{2'b11, {(ADDR_WIDTH-1){1'b0}}};
assign full_next =
(wgray_next == (rgray_wclk_sync2 ^ FULL_COMPARE_MASK));
这里 FULL_COMPARE_MASK 将最高两位置 1,其余位为 0。异或操作等价于将读格雷码的最高两位取反,再与写格雷码比较。
4.3 空判断
空判断相对简单:同步后的写格雷码与读格雷码完全相等即为空。
verilog
assign empty_next = (rgray_next == wgray_rclk_sync2);
5. 代码逐模块解析
5.1 参数与端口定义
verilog
module async_fifo #(
parameter integer DATA_WIDTH = 32,
parameter integer DEPTH = 32,
parameter integer ALMOST_EMPTY_THRESHOLD = 1
) (
input wire rstn,
input wire wclk,
input wire wr,
input wire [DATA_WIDTH-1:0] din,
output reg full,
input wire rclk,
input wire rd,
output reg [DATA_WIDTH-1:0] dout,
output reg empty,
output reg almost_empty
);
三个参数分别控制数据位宽、FIFO 深度和「几乎空」阈值。DEPTH 必须是 2 的幂,代码在仿真阶段通过 $fatal 做了合法性检查。
5.2 异步复位同步释放
两个时钟域各自需要独立的复位同步电路。直接使用异步复位会导致复位释放时刻不确定,可能引起亚稳态。代码采用两级同步器实现「异步复位、同步释放」:
verilog
always @(posedge wclk or negedge rstn) begin
if (!rstn)
wrstn_sync <= 2'b00;
else
wrstn_sync <= {wrstn_sync[0], 1'b1};
end
assign wrstn = wrstn_sync[1];
复位释放时,wrstn_sync 从 00 变为 01 再变为 11,wrstn 在第二个时钟沿之后才变为高电平,确保复位释放与写时钟同步。
5.3 指针同步器
读格雷码同步到写域用于满判断,写格雷码同步到读域用于空判断。每个同步器都是两级寄存器,并标注了 ASYNC_REG 综合属性,提示工具将其放在同一个 SLICE 内以缩短布线延迟:
verilog
(* ASYNC_REG = "TRUE" *) reg [PTR_WIDTH-1:0] rgray_wclk_sync1;
(* ASYNC_REG = "TRUE" *) reg [PTR_WIDTH-1:0] rgray_wclk_sync2;
5.4 读写使能与指针更新
读写操作只有在非空/非满且复位完成时才被接受:
verilog
assign write_enable = wr && !full && wrstn;
assign read_enable = rd && !empty && rrstn;
指针更新采用「先计算下一值,再在时钟沿锁存」的方式,避免组合逻辑中的毛刺影响:
verilog
assign wbin_next = wbin + write_enable;
assign wgray_next = (wbin_next >> 1) ^ wbin_next;
5.5 剩余数据量计算
代码在读时钟域计算剩余数据量,用于产生 almost_empty 标志。由于写指针是同步后的值,存在两个时钟周期的延迟,因此计算出的剩余量偏保守,不会出现「误报非空」的情况:
verilog
assign remaining_next = wbin_rclk_sync - rbin_next;
assign almost_empty_next = (remaining_next <= ALMOST_EMPTY_THRESHOLD);
这里使用固定位宽减法,指针回绕时自动取模,无需额外处理。
5.6 双端口 RAM 与同步读
RAM 使用写时钟驱动写端口,读时钟驱动读端口。写操作不复位 RAM,便于综合工具推断为块 RAM(Block RAM):
verilog
reg [DATA_WIDTH-1:0] memory [0:DEPTH-1];
always @(posedge wclk) begin
if (write_enable)
memory[wbin[ADDR_WIDTH-1:0]] <= din;
end
always @(posedge rclk) begin
if (read_enable)
dout <= memory[rbin[ADDR_WIDTH-1:0]];
end
注意读操作是同步读 :rd 在上升沿被接受,dout 在该沿之后更新并保持。dout 不复位,只在第一次成功读取之后才有意义。
6. 仿真验证案例
下面给出一个简单的 testbench,验证基本读写、空满标志和跨时钟域行为:
verilog
`timescale 1ns/1ps
module tb_async_fifo;
reg rstn;
reg wclk;
reg rclk;
reg wr;
reg [31:0] din;
wire full;
reg rd;
wire [31:0] dout;
wire empty;
wire almost_empty;
async_fifo #(
.DATA_WIDTH(32),
.DEPTH(8),
.ALMOST_EMPTY_THRESHOLD(1)
) u_fifo (
.rstn(rstn),
.wclk(wclk),
.wr(wr),
.din(din),
.full(full),
.rclk(rclk),
.rd(rd),
.dout(dout),
.empty(empty),
.almost_empty(almost_empty)
);
// 写时钟 100MHz,读时钟 75MHz,模拟异步时钟域
initial wclk = 0;
always #5 wclk = ~wclk;
initial rclk = 0;
always #6.667 rclk = ~rclk;
integer i;
initial begin
rstn = 0;
wr = 0;
rd = 0;
din = 0;
#100;
rstn = 1;
#20;
// 写入 8 个数据,填满 FIFO
for (i = 0; i < 8; i = i + 1) begin
@(posedge wclk);
wr = 1;
din = i;
end
@(posedge wclk);
wr = 0;
// 等待满标志
wait (full == 1);
$display("FIFO is full at time %0t", $time);
// 读出 8 个数据
for (i = 0; i < 8; i = i + 1) begin
@(posedge rclk);
rd = 1;
end
@(posedge rclk);
rd = 0;
wait (empty == 1);
$display("FIFO is empty at time %0t", $time);
#200;
$finish;
end
initial begin
$monitor("time=%0t wr=%b rd=%b din=%0d dout=%0d full=%b empty=%b",
$time, wr, rd, din, dout, full, empty);
end
endmodule
仿真结果应显示:写入 8 个数据后 full 拉高;读出 8 个数据后 empty 拉高;dout 依次输出 0 到 7。
7. 工程实践建议
7.1 时序约束
异步 FIFO 的跨时钟域路径需要设置合理的时序约束。对于格雷码指针同步器,建议使用 set_clock_groups -asynchronous 声明异步时钟域,并配合 set_max_delay 约束同步器路径,确保两级同步器之间的布线延迟可控。
7.2 亚稳态与 MTBF
两级同步器可以将亚稳态的 MTBF(平均无故障时间)提升到足够高的水平,但并非绝对安全。对于高可靠性设计,可考虑:
- 使用三级同步器进一步降低失效率;
- 对同步后的指针增加冗余校验;
- 在综合工具中开启跨时钟域分析(CDC 分析)。
7.3 复位策略
本文代码采用异步复位、同步释放,这是业界推荐的做法。注意复位信号本身也需要满足恢复时间和移除时间要求,必要时对复位信号做异步复位同步释放处理。
7.4 性能优化
- 对于深度较大的 FIFO,可考虑将 RAM 拆分为多个 Bank 以降低读写冲突;
- 若读延迟敏感,可将同步读改为「先读后打拍」的流水线结构;
almost_empty阈值可根据实际吞吐需求调整,避免频繁进入「几乎空」状态导致写侧停顿。
8. 总结
本文从异步 FIFO 的基本结构出发,详细解析了一段完整 Verilog 实现的各个模块:
- 格雷码指针解决了跨时钟域多位翻转的采样错误问题;
- 指针位宽多一位的技巧区分了空与满两种状态;
- 两级同步器消除了亚稳态风险;
- 异步复位同步释放保证了复位时序的可靠性;
- 剩余数据量计算 提供了
almost_empty标志,便于上层逻辑做流量控制。
这段代码结构清晰、参数化程度高,可直接用于 FPGA 工程,也可作为学习跨时钟域设计的经典范例。理解其设计思想后,读者可以进一步扩展出「几乎满」标志、可配置深度、读写计数统计等更丰富的功能。
9. 完整代码
下面是本文讲解的完整 Verilog 异步 FIFO 代码,可直接复制使用:
verilog
`timescale 1ns/1ps
// 异步 FIFO:二进制指针访问 RAM,格雷码指针经过两级同步后跨时钟域。
// DEPTH 必须是 >= 2 的 2 的幂;读写请求仅在非空/非满时被接受。
module async_fifo #(
parameter integer DATA_WIDTH = 32,
parameter integer DEPTH = 32,
parameter integer ALMOST_EMPTY_THRESHOLD = 1
) (
input wire rstn,
input wire wclk,
input wire wr,
input wire [DATA_WIDTH-1:0] din,
output reg full,
input wire rclk,
input wire rd,
output reg [DATA_WIDTH-1:0] dout,
output reg empty,
output reg almost_empty
);
localparam integer ADDR_WIDTH = $clog2(DEPTH);
localparam integer PTR_WIDTH = ADDR_WIDTH + 1;
// 格雷码判满:同步后的读指针最高两位取反,其余位不变。
localparam [PTR_WIDTH-1:0] FULL_COMPARE_MASK =
{2'b11, {(ADDR_WIDTH-1){1'b0}}};
// 1. 每个时钟域分别实现异步复位、两级同步释放。
(* ASYNC_REG = "TRUE" *) reg [1:0] wrstn_sync;
(* ASYNC_REG = "TRUE" *) reg [1:0] rrstn_sync;
wire wrstn;
wire rrstn;
always @(posedge wclk or negedge rstn) begin
if (!rstn)
wrstn_sync <= 2'b00;
else
wrstn_sync <= {wrstn_sync[0], 1'b1};
end
always @(posedge rclk or negedge rstn) begin
if (!rstn)
rrstn_sync <= 2'b00;
else
rrstn_sync <= {rrstn_sync[0], 1'b1};
end
assign wrstn = wrstn_sync[1];
assign rrstn = rrstn_sync[1];
// 2. 本地二进制/格雷码指针。额外一位用于区分空与满。
reg [PTR_WIDTH-1:0] wbin;
reg [PTR_WIDTH-1:0] wgray;
reg [PTR_WIDTH-1:0] rbin;
reg [PTR_WIDTH-1:0] rgray;
// 读格雷码同步到写域,写格雷码同步到读域。
(* ASYNC_REG = "TRUE" *) reg [PTR_WIDTH-1:0] rgray_wclk_sync1;
(* ASYNC_REG = "TRUE" *) reg [PTR_WIDTH-1:0] rgray_wclk_sync2;
(* ASYNC_REG = "TRUE" *) reg [PTR_WIDTH-1:0] wgray_rclk_sync1;
(* ASYNC_REG = "TRUE" *) reg [PTR_WIDTH-1:0] wgray_rclk_sync2;
always @(posedge wclk or negedge wrstn) begin
if (!wrstn) begin
rgray_wclk_sync1 <= {PTR_WIDTH{1'b0}};
rgray_wclk_sync2 <= {PTR_WIDTH{1'b0}};
end else begin
rgray_wclk_sync1 <= rgray;
rgray_wclk_sync2 <= rgray_wclk_sync1;
end
end
always @(posedge rclk or negedge rrstn) begin
if (!rrstn) begin
wgray_rclk_sync1 <= {PTR_WIDTH{1'b0}};
wgray_rclk_sync2 <= {PTR_WIDTH{1'b0}};
end else begin
wgray_rclk_sync1 <= wgray;
wgray_rclk_sync2 <= wgray_rclk_sync1;
end
end
// 3. 组合逻辑计算下一指针和下一空满状态。
wire write_enable;
wire read_enable;
wire [PTR_WIDTH-1:0] wbin_next;
wire [PTR_WIDTH-1:0] wgray_next;
wire [PTR_WIDTH-1:0] rbin_next;
wire [PTR_WIDTH-1:0] rgray_next;
wire full_next;
wire empty_next;
assign write_enable = wr && !full && wrstn;
assign read_enable = rd && !empty && rrstn;
assign wbin_next = wbin + write_enable;
assign wgray_next = (wbin_next >> 1) ^ wbin_next;
assign rbin_next = rbin + read_enable;
assign rgray_next = (rbin_next >> 1) ^ rbin_next;
assign full_next =
(wgray_next == (rgray_wclk_sync2 ^ FULL_COMPARE_MASK));
assign empty_next = (rgray_next == wgray_rclk_sync2);
// 4. 在读域计算剩余数据量,包含本周期实际接受的读取。
wire [PTR_WIDTH-1:0] wbin_rclk_sync;
wire [PTR_WIDTH-1:0] remaining_next;
wire almost_empty_next;
genvar bit_index;
generate
for (bit_index = 0; bit_index < PTR_WIDTH;
bit_index = bit_index + 1) begin : gray_to_binary
assign wbin_rclk_sync[bit_index] =
^wgray_rclk_sync2[PTR_WIDTH-1:bit_index];
end
endgenerate
// 固定位宽减法自然处理指针回绕;同步延迟使数量估计偏保守。
assign remaining_next = wbin_rclk_sync - rbin_next;
assign almost_empty_next =
(remaining_next <= ALMOST_EMPTY_THRESHOLD);
// 5. 时序逻辑保存状态。即使没有读写请求,也重新更新标志。
always @(posedge wclk or negedge wrstn) begin
if (!wrstn) begin
wbin <= {PTR_WIDTH{1'b0}};
wgray <= {PTR_WIDTH{1'b0}};
full <= 1'b0;
end else begin
wbin <= wbin_next;
wgray <= wgray_next;
full <= full_next;
end
end
always @(posedge rclk or negedge rrstn) begin
if (!rrstn) begin
rbin <= {PTR_WIDTH{1'b0}};
rgray <= {PTR_WIDTH{1'b0}};
empty <= 1'b1;
almost_empty <= 1'b1;
end else begin
rbin <= rbin_next;
rgray <= rgray_next;
empty <= empty_next;
almost_empty <= almost_empty_next;
end
end
// 6. 双时钟 RAM:当前二进制指针低位作为地址,不使用下一指针。
// 不复位 RAM,利于推断 FPGA 块 RAM;复位后旧数据由指针逻辑作废。
reg [DATA_WIDTH-1:0] memory [0:DEPTH-1];
always @(posedge wclk) begin
if (write_enable)
memory[wbin[ADDR_WIDTH-1:0]] <= din;
end
// 同步读:有效 rd 在上升沿被接受,dout 在该沿之后更新并保持。
// dout 不复位,仅在第一次成功读取之后有意义。
always @(posedge rclk) begin
if (read_enable)
dout <= memory[rbin[ADDR_WIDTH-1:0]];
end
// 参数合法性检查,仅用于仿真,不生成硬件。
// 实际实现还需约束异步时钟和格雷码总线的延迟/偏斜。
`ifndef SYNTHESIS
initial begin
if (DATA_WIDTH < 1 || DEPTH < 2 || (DEPTH & (DEPTH-1)) != 0)
$fatal(1, "DATA_WIDTH must be positive; DEPTH must be a power of two >= 2.");
if (ALMOST_EMPTY_THRESHOLD < 0 || ALMOST_EMPTY_THRESHOLD > DEPTH)
$fatal(1, "ALMOST_EMPTY_THRESHOLD must be between 0 and DEPTH.");
end
`endif
endmodule