FPGA 复位回归实验:双时钟停钟、释放中重入与消费端首拍自检

复位链在单时钟、一次上电的波形里看起来正常,接入多个时钟域后却还要回答三个具体问题:一个时钟停了,另一个时钟域能否按自己的节拍退出复位;释放到一半又收到复位,是否重新等待完整链长;复位输出刚拉低的那个沿,业务计数器到底能不能开始工作。

此前的《FPGA 复位设计:同步复位 vs 异步复位,异步复位同步释放最佳实践》讨论了复位类型与两级模板。本篇围绕上面三个边界做一组独立的 RTL 自检实验:两个周期不同的目的时钟,各自实例化复位释放链,再用边沿计数参考模型、消费端模型和变异反例检查行为。

1 依赖、命令和验证边界

正文给出完整 reset_release.v 与 tb_reset_release.sv。依赖为 Vivado Simulator 的 xvlog、xelab、xsim;在已配置 Vivado 环境的终端执行:

text 复制代码
xvlog --sv reset_release.v tb_reset_release.sv
xelab tb_reset_release -s reset_snapshot -debug typical
xsim reset_snapshot -runall

--sv 必须保留:设计主体使用 Verilog 风格,仿真保护使用 $fatal,测试平台使用 SystemVerilog。这里没有实例化器件原语或 XPM 宏,因此这份行为测试不需要额外编译 XPM 库。三阶段流程见 UG900 2022.1 批处理仿真。

配套 run_fpga_tests.py 使用 Python 3.8 及以上和标准库,按用例分别保存源码、三阶段日志、源文件 SHA-256 和 summary.json。工具在 PATH 中时运行 python run_fpga_tests.py --out results,否则用 --vivado-bin 指定安装目录;输出目录必须是新目录。

2026 年 10 月 16 日,使用 Windows 11(构建 26200)、Python 3.14.5、Vivado Simulator 2022.1 64 位,XSim SW Build 3526262 完成实测。实际工具位于 D:/tools/Xilinx/Vivado/2022.1/bin。本篇主回归的 1 个正例、5 个变异反例、3 个参数反例全部符合各自预期,两个时钟域各覆盖 2、3、4 级释放链。

text 复制代码
TRACE stopped_clock a_rst=000 b_rst=111
CASE_PASS reset domain=0 stages=2,3,4 edges=84 assertions=7
CASE_PASS reset domain=1 stages=2,3,4 edges=52 assertions=7
TEST_PASS reset_release six configurations, two domains, stopped clock

这里 assertions=7 是外部复位 arst 的置位次数,不是断言检查数量;edges=84/52 是各域观察到的时钟上升沿数。A 域在 B 停钟时已经撤销复位,B 域仍保持三个输出为 1,与 TRACE 一致。独立补充 TB 又枚举 STAGES=2...8,通过 1099 次输出检查和 9 轮重入检查,完整代码见后文。

本次最终双模块套件重跑 17/17 项符合预期;全部功能反例与参数反例都先成功编译、展开,再命中预定 Fatal。没有进行综合、布局布线、恢复/移除时序分析、MTBF 计算或硬件测试,不能把通过记录当成真实亚稳态或物理脉宽已经验证。

2 先写清这次实验的契约

arst 和 rst 都是高有效。arst=1 时,整条链异步置 1,不必等待目的时钟;arst=0 后,每个目的时钟上升沿向链中移入一个 0。理想 RTL 中,第 STAGES 个上升沿之后,输出 rst 才变成 0。

STAGES 在本例限制为 2 到 8,默认取 3。这个范围是本文接口的约定,不是对所有器件、频率和可靠性目标推荐同一个级数。正文主测试覆盖 2、3、4 级,补充测试枚举 2 到 8 级,非法参数测试覆盖 0、1、9;这些用例也不等于穷举所有时钟、输入和物理工作条件。

同一个外部复位可以送进多个链的 arst,但每个输出只服务自己的目的时钟域。某个域的时钟不再产生上升沿时,该域不能继续完成同步释放。另一个域仍按自己的时钟运行,两者没有"同一物理时刻一起退出"的保证。

这里也校正基础篇中容易产生误解的选型表述:AMD UG949 在需要复位时推荐优先考虑同步复位;不能一概认为同步复位必然多耗 LUT。确需异步复位时,应同步其撤销并检查恢复、移除时序。同步链降低亚稳态向后传播的风险,不能承诺绝对没有亚稳态。本实验只观察该结构的数字行为。UG949 2023.1:Synchronous Reset vs. Asynchronous Reset

3 三个值得单独检查的边界

3.1 链长为 3,不等于业务第三拍就工作

令 pipe[2] 为输出,复位置位后 pipe=111。外部复位在两个沿之间撤销,接下来的状态如下:

目的时钟沿 沿后 pipe 沿后 rst 示例业务计数器沿后值
E1 110 1 0
E2 100 1 0
E3 000 0 0
E4 000 0 1

业务计数器在 E3 读取到的是沿前 rst=1,因此该沿仍执行复位分支。输出在 E3 的 NBA 更新后撤销,业务在 E4 才首次更新。不能用"波形上 rst 在 E3 变为 0"推断同一个沿里所有下游都已运行。

3.2 释放中重新置位,要重新装满整条链

若已经移了两拍,pipe=100,此时 arst 再次变为 1,链必须重新变成 111。下一次撤销后重新数 E1、E2、E3。保留之前的释放进度可能让第二次复位过早撤销。

测试平台安排了短复位脉冲和释放中途的再次置位。这只是在数字仿真里检查"新的复位事件是否重新开始链的释放";短脉冲在实际器件上能否被可靠识别,要查看器件数据手册中的异步置位/复位脉宽要求,不能由几纳秒的 RTL 激励直接得出结论。官方异步复位同步宏同样要求按目标器件检查最小输入脉宽。UG953 2022.1:XPM_CDC_ASYNC_RST

3.3 一个域停钟,另一个域继续释放

实验中 A 时钟周期为 10 ns,B 时钟周期为 14 ns。暂停 B 后再次产生外部复位,A 的释放链继续移零,B 则一直保持输出复位。恢复 B 后,它要等待自己的 STAGES 个有效上升沿。

如果系统必须等所有域准备好才允许交易,应另外设计就绪握手和跨域传递。把 A 域的 rst 直接拿去释放 B 域,或者假定两个域都会同时退出,都不满足本实验的独立释放契约。

4 完整 RTL

保存为 reset_release.v。移位使用 pipe << 1,避免在非法 STAGES 下先产生负下标切片,使参数保护能够以清楚的错误信息终止仿真。

verilog 复制代码
`timescale 1ns/1ps

// Active-high asynchronous assertion, destination-clock synchronous release.
// One instance is required per destination clock domain.
module reset_release #(
    parameter integer STAGES = 3
) (
    input  wire clk,
    input  wire arst,
    output wire rst
);
    // Keep invalid configurations syntactically legal so the guard can report.
    localparam integer S = (STAGES < 1) ? 1 : STAGES;
    (* ASYNC_REG = "TRUE", SHREG_EXTRACT = "NO" *) reg [S-1:0] pipe;

    // synthesis translate_off
    initial begin
        if ((STAGES < 2) || (STAGES > 8))
            $fatal(1,"PARAM_GUARD: reset_release requires 2 <= STAGES <= 8, STAGES=%0d",STAGES);
    end
    // synthesis translate_on

    always @(posedge clk or posedge arst) begin
        if (arst)
            pipe <= {S{1'b1}};
        else
            pipe <= pipe << 1; // shift zero toward the output stage
    end
    assign rst = pipe[S-1];
endmodule

ASYNC_REG 是面向实现工具的同步链属性;AMD 文档说明它会影响优化与放置,帮助保留并靠近放置同步寄存器。把属性写进 RTL 不等于本文已经证明综合工具识别了该链,更不等于可靠性自动达标。UG912 2022.1:ASYNC_REG

这里同时给出 SHREG_EXTRACT="NO",表达保持寄存器链的实现意图。目标器件工程中应检查综合后的结构、属性是否保留以及相关报告。本文没有给出通用的 set_false_path 模板:把复位相关路径全部屏蔽,可能把需要审查的恢复、移除或域内释放路径一起藏起来。

若实际工程使用 AMD 平台提供的 XPM_CDC_ASYNC_RST,应按对应版本和目标架构的参数说明集成。本文的小模块便于解释、变异和测试,并未宣称与厂商宏在初始化选项、实现属性或全部器件上的行为完全等价。

5 完整自检 TB:不把移位代码再抄一遍当参考答案

保存为 tb_reset_release.sv。每个 reset_case 使用"外部复位撤销后经过了多少目的时钟沿"作为参考状态,达到 STAGES 才期望撤销输出。同时放一个真正消费 rst 的业务计数器,检查第一个可工作沿。

检查分为三层:

  • 每个目的上升沿和外部置位之后,比对 rst 和业务计数器
  • 每次 rst 下降时,检查它发生在该域的上升沿时刻
  • 停钟窗口结束前,明确检查 A 已退出、B 仍保持复位

异步激励避开两个时钟的上升沿,检查延迟 #0.2 用于等待 NBA 更新完成。这样测试的是明确的接口行为,而不是用同一仿真时刻的调度竞争制造"偶发现象"。

systemverilog 复制代码
`timescale 1ns/1ps

module reset_case #(parameter integer STAGES=3, DOMAIN=0) (
    input wire clk,
    input wire arst
);
    wire rst;
    integer released_edges=0, edges=0, assertions=0;
    integer work_count=0, expected_work=0;
    reg expected_rst=1;
    reg initialized=0;
    reg previous_expected_rst=1;
    realtime last_rise=-1;
    reset_release #(.STAGES(STAGES)) dut(.*);

    // Example consumer: remains reset on the edge that releases rst.
    always @(posedge clk or posedge rst) begin
        if (rst) work_count <= 0;
        else work_count <= work_count+1;
    end

    // Reference model counts destination edges instead of copying the shift RTL.
    always @(posedge clk or posedge arst) begin
        if (arst) begin
            released_edges=0;
            expected_rst=1;
            expected_work=0;
            initialized=1;
        end else if (initialized) begin
            previous_expected_rst=expected_rst;
            released_edges=released_edges+1;
            expected_rst=(released_edges < STAGES);
            if (previous_expected_rst) expected_work=0;
            else expected_work=expected_work+1;
        end
        #0.2;
        if (initialized && rst !== expected_rst)
            $fatal(1,"RESET_ASSERT domain=%0d stages=%0d t=%0t edges_since_release=%0d actual=%b expected=%b",DOMAIN,STAGES,$time,released_edges,rst,expected_rst);
        if (initialized && work_count !== expected_work)
            $fatal(1,"RESET_CONSUMER_ASSERT domain=%0d stages=%0d t=%0t actual=%0d expected=%0d",DOMAIN,STAGES,$time,work_count,expected_work);
    end

    always @(posedge clk) begin
        last_rise=$realtime;
        edges=edges+1;
    end
    always @(posedge arst) assertions=assertions+1;
    always @(negedge rst) begin
        if (initialized && ($realtime != last_rise))
            $fatal(1,"RESET_EDGE_ASSERT domain=%0d stages=%0d t=%0t",DOMAIN,STAGES,$time);
    end
endmodule

module tb_reset_release;
    reg clk_a=0, clk_b=0, enable_b=1, arst=0;
    always #5 clk_a=~clk_a;
    // Testbench-only stopped clock. This is not synthesizable clock-gating RTL.
    always begin
        #7;
        if (enable_b) clk_b=~clk_b;
        else clk_b=0;
    end
    reset_case #(.STAGES(2),.DOMAIN(0)) a2(clk_a,arst);
    reset_case #(.STAGES(3),.DOMAIN(0)) a3(clk_a,arst);
    reset_case #(.STAGES(4),.DOMAIN(0)) a4(clk_a,arst);
    reset_case #(.STAGES(2),.DOMAIN(1)) b2(clk_b,arst);
    reset_case #(.STAGES(3),.DOMAIN(1)) b3(clk_b,arst);
    reset_case #(.STAGES(4),.DOMAIN(1)) b4(clk_b,arst);

    initial begin
        // All asynchronous transitions avoid coinciding with positive edges.
        #2.3 arst=1;
        #21 arst=0;
        #90 arst=1;
        #4 arst=0;   // assertion shorter than either period, visible to RTL
        #11 arst=1;  // reassert before all chains have completed release
        #6 arst=0;
        #100;
        @(negedge clk_b); enable_b=0;
        #2.3 arst=1;
        #4 arst=0;
        #100;
        if ({b2.rst,b3.rst,b4.rst} !== 3'b111)
            $fatal(1,"RESET_STOP_ASSERT stopped domain released reset");
        if ({a2.rst,a3.rst,a4.rst} !== 3'b000)
            $fatal(1,"RESET_INDEPENDENCE_ASSERT running domain stayed in reset");
        $display("TRACE stopped_clock a_rst=%b%b%b b_rst=%b%b%b",a2.rst,a3.rst,a4.rst,b2.rst,b3.rst,b4.rst);
        enable_b=1;
        #150;
        if ({a2.rst,a3.rst,a4.rst,b2.rst,b3.rst,b4.rst} !== 6'b000000)
            $fatal(1,"RESET_RESTART_ASSERT");
        // Repeated warm resets at several phases relative to the two clocks.
        #2 arst=1; #9 arst=0;
        #86 arst=1; #3 arst=0;
        #93 arst=1; #8 arst=0;
        #140;
        $display("CASE_PASS reset domain=0 stages=2,3,4 edges=%0d assertions=%0d",a2.edges,a2.assertions);
        $display("CASE_PASS reset domain=1 stages=2,3,4 edges=%0d assertions=%0d",b2.edges,b2.assertions);
        $display("TEST_PASS reset_release six configurations, two domains, stopped clock");
        $finish;
    end
    initial begin
        #3000;
        $fatal(1,"RESET_TIMEOUT_ASSERT");
    end
endmodule

TB 中暂停 B 时钟的 enable_b 只是激励生成方法,不是给硬件设计推荐的门控时钟写法。时钟仍在高电平时随意切断也不是这个实验要考察的行为,因此测试先等待下降沿再暂停。

last_rise 记录仿真时间,negedge rst 检查释放事件与目的时钟沿重合;这只能检查 RTL 事件调度。真实器件的 clock-to-Q、复位布线延迟、时钟偏斜不会以这个理想事件模型完整呈现。

6 五个单缺陷变体和三种非法参数

每个反例都从原版 RTL 新复制一份,只修改一处,再运行同一 TB:

变体 修改 希望捕获的错误
reset_sync_assert 从敏感表删除 posedge arst 置位也要等时钟,停钟域不能及时复位
reset_early 输出从 pipe[S-1] 改接 pipe[S-2] 释放少等待一拍
reset_wrong_direction pipe << 1 改为 pipe >> 1 最高位在第一拍就移入 0
reset_bypass 输出直接接 arst 外部撤销直接穿透,不再按目的沿释放
reset_no_release 移位改为 pipe <= pipe 外部撤销后永不释放

实际首个失败点如下:

变体 XSim 实际停止时间 失败状态
reset_sync_assert 2.5 ns B域4级,首次异步置位后仍为X,期望1
reset_early 25.2 ns A域2级,仅经过1个释放沿就输出0
reset_wrong_direction 25.2 ns A域4级,第一释放沿后输出0
reset_bypass 23.3 ns B域4级,输出跟随外部撤销,在非目的上升沿释放
reset_no_release 35.2 ns A域2级,已满2个释放沿仍输出1

表内时间取自日志的 Time: 行,而不是错误消息里由 $time 打印的取整字段。例如第一项消息中 t=3000,紧随其后的精确停止时间为 Time: 2500 ps。本 TB 的检查还包含 #0.2,因此25 ns上升沿对应的失败检查时刻为25.2 ns。

第一项在首次异步置位时就被发现,没有运行到停钟段;这仍是对被破坏契约的有效检出。反例事实按首个断言记录,不能为了叙述方便改写成更靠后的失败波形。

参数反例分别实例化 STAGES=0、1、9,实际均在 0 ps 命中 PARAM_GUARD。这三个值仍能形成合法的声明宽度,避免把"语法错误"误算成"参数保护正常"。

单独复现参数保护,可使用下面顶层,依次替换 STAGES 的值,并把 xelab 的顶层改成 tb_guard:

systemverilog 复制代码
`timescale 1ns/1ps
module tb_guard;
    wire rst;
    reset_release #(.STAGES(1)) dut(
        .clk(1'b0), .arst(1'b0), .rst(rst)
    );
    initial begin
        #20;
        $display("GUARD_MISSING");
        $finish;
    end
endmodule

参数保护被综合忽略,因此仍要在工程入口检查参数是否合法。负例只有在编译、展开成功,并命中预定的功能断言或参数保护,且没有 TEST_PASS 时才算符合预期;工具启动失败或语法错误不算检出功能缺陷。本次 XSim 对故意失败的仿真也返回0,实际判断同时要求对应Fatal与没有TEST_PASS,不能只读退出码。

7 补充复核:枚举合法级数与各个重入阶段

另一个 TB 不使用自由运行时钟,主动发出每个目的沿,枚举 STAGES=2...8。先在完全无时钟时检查置位和保持,再在0...8个释放沿之后重新置位,检查必须重新开始等待。保存为 tb_reset_review.sv:

text 复制代码
xvlog --sv reset_release.v tb_reset_review.sv
xelab tb_reset_review -s reset_review_snapshot -debug typical
xsim reset_review_snapshot -runall
systemverilog 复制代码
`timescale 1ns/1ps
module tb_reset_review;
  logic clk=0,arst=0;
  wire [8:2] rst;
  integer checks=0,release_edges=0,rounds=0;
  for(genvar s=2;s<=8;s++)begin:g
    reset_release #(.STAGES(s)) dut(.clk(clk),.arst(arst),.rst(rst[s]));
  end
  task automatic check(input integer elapsed_i);
    for(int s=2;s<=8;s++)begin
      checks++;
      if(rst[s] !== (elapsed_i<s))
        $fatal(1,"REVIEW_RESET_ASSERT stages=%0d elapsed=%0d got=%b",s,elapsed_i,rst[s]);
    end
  endtask
  task automatic tick(input integer elapsed_i);
    clk=1;#1;check(elapsed_i);clk=0;#1;
  endtask
  initial begin
    // Assertion without a running clock; deassertion alone cannot release.
    #1;arst=1;#1;check(0);
    tick(0);tick(0);
    arst=0;#20;check(0);
    for(int k=1;k<=9;k++)tick(k);
    // Reassert at every partial-release boundary and require a fresh chain.
    for(int phase_i=0;phase_i<=8;phase_i++)begin
      arst=1;#1;check(0);arst=0;#1;
      for(int k=1;k<=phase_i;k++)tick(k);
      #0.1;arst=1;#0.1;check(0);arst=0;#10;check(0);
      for(int k=1;k<=9;k++)tick(k);
      rounds++;
    end
    $display("TEST_PASS independent reset boundary review stages=2..8 checks=%0d reassert_rounds=%0d",checks,rounds);
    $finish;
  end
  initial begin #5000;$fatal(1,"REVIEW_TIMEOUT_RESET");end
endmodule

实际输出:

text 复制代码
TEST_PASS independent reset boundary review stages=2..8 checks=1099 reassert_rounds=9

其中0.1 ns脉冲只用于数字事件测试,不能作为目标器件允许同样物理脉宽的证据。本文讨论的STAGES范围也只是自定义模块的参数契约,不表示更长同步链在硬件上普遍非法。

8 波形检查:把两个域放在同一个窗口

在已经成功展开的目录执行 xsim reset_snapshot -gui。将顶层 arst、clk_a、clk_b、enable_b,以及 a3/dut/pipe、a3/rst、a3/work_count、b3/dut/pipe、b3/rst、b3/work_count 加到波形中。

  1. 先看第一次外部复位的撤销:两个域各自从第一有效沿开始数,不要求它们在同一时刻拉低 rst。
  2. 放大 STAGES=3 的释放沿:pipe 由 100 变 000 时,work_count 仍为 0,下一目的沿才为 1。
  3. 查看释放中再次拉高 arst:两条链应重新装入全 1,业务计数归零。
  4. 定位 enable_b=0 的区间:B 时钟停止,B 的复位维持;A 仍能移零并开始工作。
  5. 恢复 B 后只数 B 的上升沿,确认它完成自己的链长后才撤销复位。

不要以"波形里没有 X"判断已经验证了亚稳态。这里没有模拟触发器的模拟态持续时间,也没有建立器件相关的恢复、移除违例模型。即使所有断言通过,也只能说明这些数字激励下的行为符合契约;硬件中的释放延迟还可能受同步过程影响,不能把理想 RTL 的精确拍数当作模拟可靠性保证。

9 把回归结果接到真实工程还缺什么

实际接入时,应确认外部复位来源和最小脉宽、目的时钟何时稳定、每个复位输出服务哪些寄存器、同步链属性是否被识别,以及跨域数据在启动期间有没有协议约束。再检查实现后的复位相关时序和器件适用的可靠性报告。

多时钟系统尤其要把"本域已退出复位"和"系统可以开始收发数据"分开定义。如果 B 域时钟一直没有恢复,你的 A 域应该继续工作并等待 B 的就绪握手,还是整个系统都保持禁止交易?这个选择应该进入顶层协议和回归检查,而不能只由两条复位链的波形代替。

延伸阅读:《FPGA 复位设计:同步复位 vs 异步复位,异步复位同步释放最佳实践》。你的系统是否已把各域就绪握手加入启动协议?

相关推荐
szxinmai主板定制专家2 小时前
ZYNQ 数据采集方案踩坑总结:电力强电磁干扰、工业现场多协议适配优化
运维·人工智能·fpga开发·机器人·zynq
幻智星科技3 小时前
一个开源的纯FPGA网口采集卡
网络协议·fpga开发·udp
bobuddy4 小时前
硬件加速三件套:emulator-FPGA 原型-云仿真
fpga开发
szxinmai主板定制专家9 小时前
软硬协同!ZYNQ 高速多通道同步采集,变电站 / 工厂设备状态监测方案详解
人工智能·深度学习·机器学习·fpga开发·zynq·rk3576
FPGA小徐9 小时前
FPGA 8阶LFSR加扰/解扰器设计
fpga开发
逐光者93311 小时前
超前进位加法器:原理、推导与 Verilog 实现
fpga开发
szxinmai主板定制专家11 小时前
ZYNQ PL+PS 架构拆解|工业自动化与电力物联网高速数据采集、预处理与协议上云
arm开发·人工智能·物联网·fpga开发·数据采集卡·rk3576
szxinmai主板定制专家12 小时前
告别 ARM 实时性短板:基于 ZYNQ 的电力故障录波 + 工业振动数据采集系统设计
arm开发·人工智能·fpga开发·zynq·数据采集卡·arm+fpga·rk3588+fpga
FPGA小徐14 小时前
FPGA 实现 Canny 边缘检测|大白话原理 + 专业工程解析
fpga开发