Verilog 异步 FIFO 设计详解:格雷码跨时钟域与空满判断

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,四位全部变化。如果此时恰好被另一个时钟域采样,同步器可能采到 00001111 等任意中间值,导致指针严重错误。

格雷码(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_sync00 变为 01 再变为 11wrstn 在第二个时钟沿之后才变为高电平,确保复位释放与写时钟同步。

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
相关推荐
深圳市恒锐丰科技杨生2 小时前
EG6288 220V 三相独立半桥栅极驱动芯片|屹晶 EGmicro
嵌入式硬件·硬件工程
free-elcmacom9 小时前
FPGA学习笔记<1>FPGA的开发流程
fpga开发·硬件工程·hdl
科恒盛远17 小时前
ZU902 FMC194 可编程宽带信号源实测
fpga开发·硬件工程·信号处理
深圳市恒锐丰科技杨生1 天前
EG1192H 零功耗使能集成上管非同步降压 DC‑DC|屹晶 EGmicro
嵌入式硬件·硬件工程
粟米茶1 天前
MOS 选型要点(八):开通/关断过程与负载类型的关系
硬件工程·硬件电路·mosfet
粟米茶3 天前
MOS 选型要点(四):阈值电压 VGS (th)、结电容与栅极电荷
硬件工程·硬件电路·mosfet
三块石头1013 天前
车载ADC避坑:为什么ADC采集值偏低?
经验分享·stm32·单片机·嵌入式硬件·mcu·硬件架构·硬件工程
服务器硬件项目经理3 天前
第14篇_服务器主板上MOSFET的三大应用
运维·服务器·人工智能·单片机·硬件工程·硬件
HUI-4743 天前
EG1170|SSOP‑10 外置 MOS 200V 异步降压控制器,快充 / 电池充电大电流降压优选
嵌入式硬件·硬件工程