📝

第五阶段:仿真、约束与 FPGA 工程实践

面向安路 FPGA 与 TangDynasty 工具链,完成 Testbench、引脚约束、时序约束、实现和上板验证闭环。

返回 FPGA 与 Verilog 学习目录

第五阶段:仿真、约束与 FPGA 工程实践

本阶段面向安路 FPGA 与 TangDynasty(简称 TD)工具链。示例参考本机已有的 EG4S20BG256 板卡工程和 50 MHz 板载时钟。

.adc 中的封装引脚只适用于对应板卡。更换开发板、FPGA 型号或封装后,必须重新查原理图,不能照抄。


本阶段解决什么问题

前四个阶段主要学习“怎样用 RTL 描述数字电路”,第五阶段建立三个工程闭环:

1
2
3
4
5
6
功能闭环:RTL -> 自检式 Testbench -> PASS/FAIL

约束闭环:顶层端口 -> 原理图网络 -> FPGA 封装引脚 -> .adc
          外部时钟/接口时序 -> .sdc -> 时序报告

上板闭环:综合 -> 布局布线 -> 静态时序分析 -> 位流 -> 下载 -> 板级验证

只完成 RTL 不算完成 FPGA 工程;只生成位流也不代表工程可靠。

学习目标

完成本阶段后,应当能够:

  • 编写具有期望值、错误计数、超时保护和 PASS/FAIL 的 Testbench
  • 正确安排时序 Testbench 的驱动时刻和检查时刻
  • 根据开发板原理图独立编写安路 .adc
  • 根据晶振频率独立编写 .sdc
  • 理解 LOCATIONIOSTANDARDPULLTYPEDRIVESTRENGTH
  • 检查 .sdc 是否真正命中时钟,而不只看“没有语法错误”
  • 在 TD 中正确设置器件、顶层、设计文件和约束文件
  • 阅读综合、引脚分配和最终时序报告
  • 完成一次 .v + .adc + .sdc + bitstream 上板闭环

第 14 章:自检式 Testbench 与仿真

14.1 DUT 与 Testbench

RTL 文件描述被测硬件,Testbench 描述外部环境和检查规则。

1
2
3
4
5
6
7
8
9
Testbench
  |-- 产生时钟和复位
  |-- 驱动输入
  |-- 保存或计算期望结果
  |-- 比较 DUT 输出
  `-- 输出 PASS/FAIL
        |
        v
       DUT
文件 作用 是否综合进 FPGA
counter.v DUT,描述实际硬件
counter_tb.v 仿真激励和检查
.vcd 波形数据
.adc 引脚和 I/O 电气约束
.sdc 时钟和接口时序约束

仿真通过不能证明引脚正确、I/O 电压正确或布局布线后满足时序。这些问题在第 15 章解决。

14.2 用于练习的计数器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
module counter_mod_n #(
    parameter WIDTH = 4,
    parameter N     = 10
) (
    input  wire             clk,
    input  wire             reset_n,
    input  wire             enable,
    output reg  [WIDTH-1:0] count,
    output reg              tick
);

always @(posedge clk or negedge reset_n) begin
    if (!reset_n) begin
        count <= {WIDTH{1'b0}};
        tick  <= 1'b0;
    end else begin
        tick <= 1'b0;

        if (enable) begin
            if (count == N - 1) begin
                count <= {WIDTH{1'b0}};
                tick  <= 1'b1;
            end else begin
                count <= count + 1'b1;
            end
        end
    end
end

endmodule

要验证的完整行为是:

  1. 复位时 count=0tick=0
  2. enable=0 时保持
  3. enable=1 时递增
  4. N-1 回到 0
  5. 只有回卷周期 tick=1
  6. 参数改变后仍符合规则

14.3 Testbench 骨架

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
`timescale 1ns/1ps

module counter_mod_n_tb;

reg clk;
reg reset_n;
reg enable;

wire [2:0] count;
wire       tick;

counter_mod_n #(
    .WIDTH(3),
    .N(5)
) dut (
    .clk     (clk),
    .reset_n (reset_n),
    .enable  (enable),
    .count   (count),
    .tick    (tick)
);

initial clk = 1'b0;
always #5 clk = ~clk;       // 10 ns 周期,只存在于仿真

initial begin
    reset_n = 1'b0;
    enable  = 1'b0;
    #12;
    reset_n = 1'b1;
end

endmodule

timescale 1ns/1ps 表示延时单位为 1 ns、精度为 1 ps。它不会在 FPGA 中产生真实时钟。真实时钟来自晶振或 PLL,并由 .sdc 约束。

14.4 驱动与检查时刻

若 DUT 在 posedge clk 采样输入,Testbench 又在同一个上升沿修改输入:

1
2
@(posedge clk);
enable = 1'b1;

Testbench 与 DUT 会发生仿真竞争。初学阶段统一采用:

1
下降沿驱动输入 -> 上升沿让 DUT 采样 -> 上升沿后 #1 检查输出
1
2
3
4
5
6
@(negedge clk);
enable = 1'b1;

@(posedge clk);
#1;
// 检查 count 和 tick

上升沿后的 #1 还用于避开非阻塞赋值 <= 的更新时序。

14.5 自动检查而不是只打印

下面的代码只打印实际值,仍要靠人判断:

1
$display("count=%d", count);

自检必须同时拥有期望值、实际值、自动比较和错误计数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
integer errors;

task check_value;
    input [2:0] expected_count;
    input       expected_tick;
    begin
        #1;
        if ((count !== expected_count) || (tick !== expected_tick)) begin
            errors = errors + 1;
            $display(
                "ERROR t=%0t expected count=%0d tick=%b, got count=%0d tick=%b",
                $time, expected_count, expected_tick, count, tick
            );
        end
    end
endtask

这里用 !==,因为它能把 xz 也判为错误。

14.6 完整自检式 Testbench

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
`timescale 1ns/1ps

module counter_mod_n_tb;

reg clk;
reg reset_n;
reg enable;

wire [2:0] count;
wire       tick;

integer errors;

counter_mod_n #(
    .WIDTH(3),
    .N(5)
) dut (
    .clk     (clk),
    .reset_n (reset_n),
    .enable  (enable),
    .count   (count),
    .tick    (tick)
);

initial clk = 1'b0;
always #5 clk = ~clk;

task check_value;
    input [2:0] expected_count;
    input       expected_tick;
    begin
        #1;
        if ((count !== expected_count) || (tick !== expected_tick)) begin
            errors = errors + 1;
            $display(
                "ERROR t=%0t expected count=%0d tick=%b, got count=%0d tick=%b",
                $time, expected_count, expected_tick, count, tick
            );
        end
    end
endtask

initial begin
    errors  = 0;
    reset_n = 1'b0;
    enable  = 1'b0;

    #2;
    if ((count !== 3'd0) || (tick !== 1'b0)) begin
        errors = errors + 1;
        $display("ERROR: asynchronous reset failed");
    end

    @(negedge clk);
    reset_n = 1'b1;

    repeat (2) begin
        @(posedge clk);
        check_value(3'd0, 1'b0);
    end

    @(negedge clk);
    enable = 1'b1;

    @(posedge clk); check_value(3'd1, 1'b0);
    @(posedge clk); check_value(3'd2, 1'b0);
    @(posedge clk); check_value(3'd3, 1'b0);
    @(posedge clk); check_value(3'd4, 1'b0);
    @(posedge clk); check_value(3'd0, 1'b1);
    @(posedge clk); check_value(3'd1, 1'b0);

    @(negedge clk);
    enable = 1'b0;
    repeat (2) begin
        @(posedge clk);
        check_value(3'd1, 1'b0);
    end

    if (errors == 0)
        $display("PASS: all counter tests passed");
    else
        $display("FAIL: %0d error(s)", errors);

    $finish;
end

initial begin
    #1000;
    $display("FAIL: simulation timeout");
    $finish;
end

endmodule

14.7 测试用例怎样选

类型 计数器示例
初始状态 复位后是否为 0
普通状态 1、2、3 的递增
边界状态 N-1
状态转换 N-1 -> 0
控制禁止 enable=0 保持
控制恢复 禁止后重新使能
优先级冲突 复位与使能同时有效
参数变化 N=5N=10 分别测试

不同电路的测试重点:

  • 组合逻辑:小位宽时穷举全部输入
  • 计数器:复位、保持、边界和回卷
  • 移位寄存器:位序、首尾位和所需时钟数
  • FSM:每个状态和每条合法转移
  • RAM:写后读、地址边界、读延迟和同时读写规则
  • CDC:普通 RTL 仿真不能证明不会亚稳态,还要检查结构和约束

14.8 波形与自检的分工

自检结果回答“对不对”,波形回答“为什么”。推荐顺序:

  1. 运行完整自检
  2. 读取第一条错误信息
  3. 打开失败时刻附近的波形
  4. 同时观察输入、状态、计数器和输出
  5. 修复后重新运行全部测试

Icarus Verilog 示例:

1
2
iverilog -g2012 -s counter_mod_n_tb -o counter_mod_n_tb.vvp counter_mod_n.v counter_mod_n_tb.v
vvp counter_mod_n_tb.vvp

需要 VCD 波形时加入:

1
2
3
4
initial begin
    $dumpfile("counter_mod_n.vcd");
    $dumpvars(0, counter_mod_n_tb);
end

14.9 安路厂商 IP 的仿真边界

纯 RTL 通常可以用 Icarus Verilog、ModelSim/Questa 等验证。使用安路 PLL、RAM、I/O 或 SERDES 原语/IP 后,还需要:

  • 安路提供的仿真模型库
  • 与当前 TD/IP 版本匹配的模型
  • 正确的库编译和映射

普通仿真器能编译 Verilog,不代表它自动理解安路原语。应先验证自己的控制逻辑,再按 TD 生成文件和官方模型做集成仿真。

14.10 练习与完成标准

练习

  1. 为 4 位比较器遍历全部 16 x 16 = 256 种输入。
  2. 验证寄存器 reset > load > enable > hold 的优先级冲突。
  3. 将模 N 计数器分别设置为 N=5N=10 复测。
  4. 故意把回卷条件改为 count == N,确认 Testbench 报 FAIL,再恢复并得到 PASS。

完成标准

  • 能解释 DUT 和 Testbench 的职责
  • 能解释为何下降沿驱动、上升沿后检查
  • 能用 !== 检出 x/z
  • Testbench 具有期望值、自动比较、错误计数、PASS/FAIL 和超时保护
  • 能依据失败信息定位波形,而不是只凭肉眼看完整波形

第 15 章:TangDynasty 约束、实现与上板

15.1 完整工程流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
编写 RTL
  -> RTL 自检仿真
  -> 建立 TD 工程并选择完整器件型号
  -> 添加设计源文件并设置顶层
  -> 添加 .adc 和 .sdc
  -> 综合 Synthesis
  -> 布局布线 Physical Design
  -> 静态时序分析 STA
  -> 生成位流
  -> 下载和板级测量
阶段 回答的问题
RTL 仿真 功能行为是否符合预期
综合 RTL 能否变成器件中的逻辑资源
.adc 顶层端口接到哪里、使用什么 I/O 属性
.sdc 电路需要满足怎样的时钟和接口时序
布局布线 资源实际放在哪里、怎样连线
STA 实际路径延时是否满足约束
上板 FPGA、PCB 和外设组成的系统是否工作

15.2 建工程前确认四项信息

  1. FPGA 完整型号和封装,例如 EG4S20BG256
  2. 开发板原理图或官方引脚表
  3. 板载晶振频率,例如 50 MHz
  4. 外设有效电平和 I/O bank 电压

“同为 EG4 系列”不能证明引脚相同,“都是 BGA256”也不能证明管脚兼容。

本机样例工程记录的是:

1
2
3
4
Family : EG4
Device : EG4S20BG256
Clock  : 50 MHz
TD     : 6.2.x 工程格式

实际板卡不一致时,以实际原理图、器件手册和当前 TD 版本为准。

15.3 顶层端口是约束入口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module led_top #(
    parameter COUNT_MAX = 25_000_000 - 1
) (
    input  wire       clk,
    input  wire       rst_n,
    output reg  [3:0] led
);

reg [24:0] counter;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        counter <= 25'd0;
        led     <= 4'b1110;
    end else if (counter == COUNT_MAX) begin
        counter <= 25'd0;
        led     <= {led[2:0], led[3]};
    end else begin
        counter <= counter + 1'b1;
    end
end

endmodule

约束文件能看到的顶层端口是:

1
clk, rst_n, led[0], led[1], led[2], led[3]

内部的 counter 不是板外引脚,不能在 .adc 中分配封装管脚。

示例按 LED 低电平点亮设计。实际有效电平必须查原理图。

15.4 .adc.sdc 的分工

.adc 是物理和 I/O 电气约束,回答:

  • clkled[0] 分别位于哪个封装引脚?
  • 使用 LVCMOS33 还是其他 I/O 标准?
  • 输入是否需要上拉?
  • 输出驱动能力是多少?

.sdc 是时序约束,回答:

  • 输入时钟周期是多少?
  • PLL 生成时钟与源时钟是什么关系?
  • 外部输入何时到达、输出何时需要稳定?
  • 哪些时钟域确实互为异步?
1
2
.adc:接到哪里、采用什么电气属性
.sdc:需要跑多快、何时采样才正确

15.5 从原理图写 .adc

正确顺序:

1
2
3
4
5
6
7
读取 Verilog 顶层端口名
  -> 在原理图中找到对应网络
  -> 沿网络找到 FPGA 封装引脚
  -> 确认 I/O bank 电压
  -> 确认默认电平和有效电平
  -> 写入 .adc
  -> 实现后检查最终 Pin Assignment 报告

先整理映射表:

顶层端口 板上网络 LOCATION 方向 I/O 标准 备注
clk 50 MHz 晶振 R7 输入 LVCMOS33 板载时钟
rst_n 复位按键 A2 输入 LVCMOS33 低有效、上拉
led[0] LED0 A4 输出 LVCMOS33 8 mA
led[1] LED1 A3 输出 LVCMOS33 8 mA
led[2] LED2 C10 输出 LVCMOS33 8 mA
led[3] LED3 B12 输出 LVCMOS33 8 mA

以上映射来自本机对应板卡 Demo,只能作为该板卡示例。

15.6 安路 .adc 语法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
set_pin_assignment { clk } {
    LOCATION = R7;
    IOSTANDARD = LVCMOS33;
    PULLTYPE = PULLUP;
}

set_pin_assignment { rst_n } {
    LOCATION = A2;
    IOSTANDARD = LVCMOS33;
    PULLTYPE = PULLUP;
}

set_pin_assignment { led[0] } {
    LOCATION = A4;
    IOSTANDARD = LVCMOS33;
    DRIVESTRENGTH = 8;
    PULLTYPE = NONE;
}

set_pin_assignment { led[1] } {
    LOCATION = A3;
    IOSTANDARD = LVCMOS33;
    DRIVESTRENGTH = 8;
    PULLTYPE = NONE;
}

set_pin_assignment { led[2] } {
    LOCATION = C10;
    IOSTANDARD = LVCMOS33;
    DRIVESTRENGTH = 8;
    PULLTYPE = NONE;
}

set_pin_assignment { led[3] } {
    LOCATION = B12;
    IOSTANDARD = LVCMOS33;
    DRIVESTRENGTH = 8;
    PULLTYPE = NONE;
}

单行写法含义相同:

1
set_pin_assignment { clk } { LOCATION = R7; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }

各属性含义:

  • set_pin_assignment:为一个顶层端口设置物理和电气属性
  • LOCATION:当前器件封装的引脚或球位
  • IOSTANDARD:I/O 电气标准,如 LVCMOS33
  • PULLTYPE:内部上拉/下拉设置;示例使用 PULLUPNONE
  • DRIVESTRENGTH:输出驱动能力;输入通常不设置

驱动能力不是越大越好。I/O 标准、上下拉和驱动能力必须结合 bank 电压、原理图和器件手册决定。

15.7 总线与名称匹配

Verilog:

1
output wire [3:0] led;

.adc 要分别约束:

1
2
3
4
set_pin_assignment { led[0] } { LOCATION = A4; IOSTANDARD = LVCMOS33; }
set_pin_assignment { led[1] } { LOCATION = A3; IOSTANDARD = LVCMOS33; }
set_pin_assignment { led[2] } { LOCATION = C10; IOSTANDARD = LVCMOS33; }
set_pin_assignment { led[3] } { LOCATION = B12; IOSTANDARD = LVCMOS33; }

注意:

  • led[0]led[1] 是不同端口
  • 位序写反会使 LED 顺序与预期相反
  • ledLEDLED0 不应视为同名
  • .adc 名称必须匹配当前顶层端口
  • 修改顶层端口后要同步检查 .adc.sdc

15.8 .adc 常见错误

  1. 把板卡丝印 LED1 当成 FPGA LOCATION
  2. 复制另一块开发板的约束。
  3. RTL 是 clk,约束却写 CLOCK
  4. 只写 LOCATION,不检查 I/O bank 电压。
  5. 不看外部电路,习惯性给所有输入加上拉。
  6. 只因能生成位流,就认为引脚约束已经生效。

15.9 时钟周期计算与 create_clock

1
2
T = 1 / f
T(ns) = 1000 / f(MHz)
频率 周期
10 MHz 100 ns
12 MHz 83.333 ns
25 MHz 40 ns
50 MHz 20 ns
100 MHz 10 ns
125 MHz 8 ns

50 MHz 时钟约束:

1
2
3
4
5
create_clock \
    -name sys_clk \
    -period 20.000 \
    -waveform {0 10.000} \
    [get_ports {clk}]
  • -name sys_clk:时序报告中的时钟名称
  • -period 20.000:周期为 20 ns,即 50 MHz
  • -waveform {0 10.000}:0 ns 上升、10 ns 下降,50% 占空比
  • [get_ports {clk}]:时钟进入 FPGA 的顶层端口

sys_clk 是时钟对象名称,clk 是 Verilog 端口名称。两者角色不同。

15.10 约束文件被读取,不等于约束命中

若 RTL 是:

1
input wire clk;

错误的 .sdc

1
create_clock -name sys_clk -period 20.000 [get_ports {sys_clk}]

正确写法:

1
create_clock -name sys_clk -period 20.000 [get_ports {clk}]

本机已有样例曾产生这样的报告:

1
2
3
4
Clock-Name   : sys_clk
C-Freq       : 50.000 MHz
Fanout       : 0
STA coverage : 0.00%

它不是“时序完美通过”,而是约束没有覆盖实际寄存器路径。检查约束是否生效,至少要看:

  1. 时钟名称和频率是否正确
  2. 有寄存器的设计中,时钟 fanout 是否合理而非 0
  3. STA coverage 是否合理而非 0.00%
  4. 报告中是否存在该时钟域的寄存器路径
  5. 是否有 unconstrained clock/path 或对象未找到的警告

15.11 RTL 频率参数不能代替 .sdc

1
parameter CLK_FREQ_HZ = 50_000_000;

这个参数只供 RTL 计算分频、延时或波特率。STA 工具不会因此知道物理时钟周期。

必须同时一致:

1
2
3
板载实际晶振频率
= RTL 中用于功能计算的频率
= .sdc 中 create_clock 的频率
  • RTL 参数错误:延时或波特率等功能错误
  • .sdc 写得比实际慢:可能放过实际不满足的路径
  • .sdc 写得比实际快:造成不必要的实现压力或时序失败

15.12 PLL 与生成时钟

TD Demo 中可见类似约束:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
create_clock \
    -name SYS_CLK \
    -period 20.000 \
    -waveform {0 10.000} \
    [get_ports {clk}]

create_generated_clock \
    -name pixel_clk \
    -source [get_ports {clk}] \
    -master_clock SYS_CLK \
    -multiply_by 4 \
    [get_nets {pixel_clk_net}]

但不能机械照抄:

  • 倍频/分频关系必须与实际 PLL 配置一致
  • 目标必须是实现后的真实时钟网络
  • 当前 TD/IP 流程可能自动生成或传播部分约束
  • 手写约束不能与 IP 自动约束重复或冲突

初学阶段先掌握输入时钟 create_clock。使用 PLL 时,再根据当前 TD 版本、IP 生成文件和时序报告处理生成时钟。

15.13 输入输出延迟

FPGA 与外部同步器件通信时,需要根据外部器件手册约束接口:

1
2
3
4
5
set_input_delay  -clock [get_clocks {sys_clk}] -max <最大输入延迟> [get_ports {data_in[*]}]
set_input_delay  -clock [get_clocks {sys_clk}] -min <最小输入延迟> [get_ports {data_in[*]}]

set_output_delay -clock [get_clocks {sys_clk}] -max <最大输出要求> [get_ports {data_out[*]}]
set_output_delay -clock [get_clocks {sys_clk}] -min <最小输出要求> [get_ports {data_out[*]}]

数值必须来自外部器件的 clock-to-out、setup/hold、接口关系和 PCB 延时预算,不能凭感觉填写。流水灯和普通按键入门工程不需要编造输入输出延迟;学习 SDRAM、ADC、DAC、摄像头等接口时再正式计算。

15.14 异步时钟与时序例外

两个真正没有固定相位关系的时钟域可声明为异步时钟组:

1
2
3
set_clock_groups -asynchronous \
    -group [get_clocks {clk_a}] \
    -group [get_clocks {clk_b}]

这不会自动使 CDC 安全。RTL 仍需正确结构:

  • 单比特电平:双触发器同步
  • 单周期脉冲:展宽、翻转同步或握手
  • 多位数据:握手、Gray 码或异步 FIFO

不要为了消除报错随意写 set_false_path。时序例外必须有设计原理依据,否则只会隐藏真实问题。

15.15 在 TD 中加入约束

不同 TD 小版本的菜单名称可能略有不同,但对象关系相同:

  1. 新建工程,选择正确的 Family、Device 和封装
  2. 添加可综合 .v 源文件
  3. 将正确模块设置为 Top Module
  4. 建立或选择 Constraint Files/Constraint Set
  5. 添加 .adc
  6. 添加 .sdc
  7. 确认约束参与当前综合和 Physical Design run
  8. 重新运行综合,不复用过期结果
  9. 运行 Physical Design
  10. 打开最终引脚和时序报告
1
2
3
4
5
6
Design Files
  `-- led_top.v

Constraint Files
  |-- top.adc
  `-- top.sdc

15.16 综合与布局布线后检查什么

综合报告

  • Top Module 是否正确
  • 顶层端口是否仍存在
  • 寄存器、RAM、PLL 等资源是否符合预期
  • 是否意外推断锁存器
  • 是否存在未驱动、多重驱动、位宽、未连接或约束对象未找到的警告

最终引脚分配

端口 本示例预期 LOCATION
clk R7
rst_n A2
led[0] A4
led[1] A3
led[2] C10
led[3] B12

还要确认 I/O 标准与 bank 电压一致。工具能生成位流不能代替最终映射检查。

15.17 Setup、Hold、Slack 与 Coverage

Setup

数据必须在采样沿之前提前稳定。最大路径过长会产生 setup violation。

Hold

数据必须在采样沿之后继续保持一段时间。最小路径过短可能产生 hold violation。

Slack

可简化理解为:

1
Slack = 要求时间 - 实际到达时间
  • Slack > 0:满足约束
  • Slack = 0:处于边界
  • Slack < 0:时序失败

TD 报告常见:

  • SWNS:setup worst negative slack
  • STNS:setup total negative slack
  • HWNS:hold worst negative slack
  • HTNS:hold total negative slack

但读数之前先确认时钟被识别、fanout 合理、STA coverage 合理、存在真实寄存器路径,并且没有关键未约束路径。

15.18 完整流水灯闭环

led_top.v

使用 50 MHz 时钟,每 0.5 秒移动一次 LED:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module led_top #(
    parameter COUNT_MAX = 25_000_000 - 1
) (
    input  wire       clk,
    input  wire       rst_n,
    output reg  [3:0] led
);

reg [24:0] counter;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        counter <= 25'd0;
        led     <= 4'b1110;
    end else if (counter == COUNT_MAX) begin
        counter <= 25'd0;
        led     <= {led[2:0], led[3]};
    end else begin
        counter <= counter + 1'b1;
    end
end

endmodule
1
2
2^24 = 16,777,216  < 25,000,000
2^25 = 33,554,432 >= 25,000,000

所以计数器需要 25 位。

led_top_tb.v

仿真时通过参数覆盖缩短等待时间:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
`timescale 1ns/1ps

module led_top_tb;

reg clk;
reg rst_n;
wire [3:0] led;

integer errors;

led_top #(
    .COUNT_MAX(3)
) dut (
    .clk   (clk),
    .rst_n (rst_n),
    .led   (led)
);

initial clk = 1'b0;
always #10 clk = ~clk;

task check_led;
    input [3:0] expected;
    begin
        #1;
        if (led !== expected) begin
            errors = errors + 1;
            $display("ERROR t=%0t expected led=%b, got=%b", $time, expected, led);
        end
    end
endtask

initial begin
    errors = 0;
    rst_n  = 1'b0;

    #2;
    if (led !== 4'b1110) begin
        errors = errors + 1;
        $display("ERROR: reset value incorrect");
    end

    @(negedge clk);
    rst_n = 1'b1;

    repeat (3) begin
        @(posedge clk);
        check_led(4'b1110);
    end

    @(posedge clk);
    check_led(4'b1101);

    repeat (3) begin
        @(posedge clk);
        check_led(4'b1101);
    end

    @(posedge clk);
    check_led(4'b1011);

    if (errors == 0)
        $display("PASS: led_top tests passed");
    else
        $display("FAIL: %0d error(s)", errors);

    $finish;
end

initial begin
    #2000;
    $display("FAIL: simulation timeout");
    $finish;
end

endmodule

top.adc

1
2
3
4
5
6
set_pin_assignment { clk }    { LOCATION = R7;  IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { rst_n }  { LOCATION = A2;  IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { led[0] } { LOCATION = A4;  IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led[1] } { LOCATION = A3;  IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led[2] } { LOCATION = C10; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led[3] } { LOCATION = B12; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }

top.sdc

1
2
3
4
5
create_clock \
    -name sys_clk \
    -period 20.000 \
    -waveform {0 10.000} \
    [get_ports {clk}]

执行顺序

  1. COUNT_MAX=3 完成自检仿真。
  2. TD 工程选择实际器件;本示例才使用 EG4S20BG256
  3. 添加 led_top.v 并设 led_top 为顶层。
  4. 添加 top.adctop.sdc 到当前约束文件集。
  5. 运行综合,检查顶层、警告和资源。
  6. 运行布局布线,检查最终引脚。
  7. 检查 sys_clk=50 MHz、fanout、coverage、setup 和 hold。
  8. 生成位流并下载。
  9. 检查复位和 LED 移动方向。
  10. 记录 TD 版本、器件、位流时间和上板结果。

15.19 上板故障分层排查

第一层:工程和下载

  • 下载器是否识别器件
  • 下载是否成功
  • 位流是否来自刚编译的工程
  • 顶层、器件型号和封装是否正确

第二层:.adc

  • 端口名是否匹配
  • LOCATION 是否来自当前板卡原理图
  • I/O 标准是否符合 bank 电压
  • LED 和按键有效电平是否正确

第三层:时钟和复位

  • 晶振实际频率是多少
  • .sdc 是否命中真实 clk
  • fanout 和 coverage 是否合理
  • 复位是否一直有效

第四层:RTL

  • Testbench 是否覆盖当前参数
  • 计数位宽是否足够
  • 终值比较是否差一
  • RTL 频率参数是否正确

第五层:测量

工具 适合观察
RTL 仿真 理想数字逻辑和 RTL 内部信号
ChipWatcher FPGA 运行时内部节点
外部逻辑分析仪 已引出管脚的数字协议和电平变化
示波器 电压、边沿、振铃、抖动和真实波形

15.20 工程验证记录

每次可复现结果至少记录:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
日期:
TD 版本:
FPGA 完整型号/封装:
开发板版本:
顶层模块:
RTL 文件版本或时间戳:
.adc 文件版本:
.sdc 文件版本:
板载时钟频率:
综合结果和关键警告:
布局布线结果:
时钟 fanout:
STA coverage:
Setup/Hold 结果:
位流生成时间:
下载结果:
板级现象:

15.21 练习与完成标准

练习

  1. 从实际原理图选择 1 个时钟、1 个按键和 4 个 LED,先做映射表,再写 .adc
  2. 分别写出 12、25、50、100 MHz 的 create_clock
  3. 故意将 [get_ports {clk}] 改为不存在的端口,对比 warning、fanout 和 coverage,再恢复。
  4. 提交流水灯的 RTL、Testbench、.adc.sdc、仿真结果、最终引脚、时序摘要和上板记录。

知识标准

  • 能说明 .adc.sdc 的区别
  • 能说明 RTL 参数为何不能代替时钟约束
  • 能解释 setup、hold、slack、fanout 和 STA coverage
  • 知道仿真通过、综合通过、时序通过和上板正常是四件事

文件标准

  • .adc 每个 LOCATION 都能追溯到实际原理图
  • 顶层端口与 .adc/.sdc 完全一致
  • .sdc 周期与晶振频率一致
  • 约束已加入当前 TD Constraint Set 并参与当前 run

验证标准

  • RTL Testbench 自动输出 PASS/FAIL
  • 无未解释的关键 warning
  • 最终引脚分配与 .adc 一致
  • 时钟 fanout 和 STA coverage 合理
  • setup、hold 检查通过
  • 位流时间戳与本次修改一致
  • 板上现象与设计一致

第五阶段总复习

必须记住的六句话

  1. Testbench 要自动判断结果,不能只打印或只看波形。
  2. 时序输入在非采样沿驱动,在采样沿之后检查。
  3. .adc 管引脚和 I/O 电气属性,.sdc 管时钟和接口时序。
  4. 顶层端口、原理图网络和封装引脚必须形成可核对的映射链。
  5. WNS=0 不一定代表通过,还要检查 fanout、coverage 和未约束路径。
  6. 仿真、约束、实现、时序和上板全部闭环,工程才算完成。

推荐学习顺序

1
2
3
4
5
6
第 1 次:完成第 14 章自检式 Testbench
第 2 次:完成顶层端口到原理图的映射表
第 3 次:编写并检查 .adc
第 4 次:计算周期并编写 .sdc
第 5 次:建立 TD 工程并读综合、引脚和时序报告
第 6 次:完成流水灯上板和验证记录

不要一次复制所有文件后直接点击“生成位流”。每次解决一个闭环,才能知道错误位于哪一层。

comments powered by Disqus