返回 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
- 理解
LOCATION、IOSTANDARD、PULLTYPE 和 DRIVESTRENGTH
- 检查
.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
|
要验证的完整行为是:
- 复位时
count=0、tick=0
enable=0 时保持
enable=1 时递增
- 从
N-1 回到 0
- 只有回卷周期
tick=1
- 参数改变后仍符合规则
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
|
这里用 !==,因为它能把 x、z 也判为错误。
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=5、N=10 分别测试 |
不同电路的测试重点:
- 组合逻辑:小位宽时穷举全部输入
- 计数器:复位、保持、边界和回卷
- 移位寄存器:位序、首尾位和所需时钟数
- FSM:每个状态和每条合法转移
- RAM:写后读、地址边界、读延迟和同时读写规则
- CDC:普通 RTL 仿真不能证明不会亚稳态,还要检查结构和约束
14.8 波形与自检的分工
自检结果回答“对不对”,波形回答“为什么”。推荐顺序:
- 运行完整自检
- 读取第一条错误信息
- 打开失败时刻附近的波形
- 同时观察输入、状态、计数器和输出
- 修复后重新运行全部测试
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 练习与完成标准
练习
- 为 4 位比较器遍历全部
16 x 16 = 256 种输入。
- 验证寄存器
reset > load > enable > hold 的优先级冲突。
- 将模 N 计数器分别设置为
N=5 和 N=10 复测。
- 故意把回卷条件改为
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 建工程前确认四项信息
- FPGA 完整型号和封装,例如
EG4S20BG256
- 开发板原理图或官方引脚表
- 板载晶振频率,例如 50 MHz
- 外设有效电平和 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 电气约束,回答:
clk、led[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:内部上拉/下拉设置;示例使用 PULLUP 或 NONE
DRIVESTRENGTH:输出驱动能力;输入通常不设置
驱动能力不是越大越好。I/O 标准、上下拉和驱动能力必须结合 bank 电压、原理图和器件手册决定。
15.7 总线与名称匹配
Verilog:
.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 顺序与预期相反
led、LED、LED0 不应视为同名
.adc 名称必须匹配当前顶层端口
- 修改顶层端口后要同步检查
.adc 和 .sdc
15.8 .adc 常见错误
- 把板卡丝印
LED1 当成 FPGA LOCATION。
- 复制另一块开发板的约束。
- RTL 是
clk,约束却写 CLOCK。
- 只写 LOCATION,不检查 I/O bank 电压。
- 不看外部电路,习惯性给所有输入加上拉。
- 只因能生成位流,就认为引脚约束已经生效。
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 是:
错误的 .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%
|
它不是“时序完美通过”,而是约束没有覆盖实际寄存器路径。检查约束是否生效,至少要看:
- 时钟名称和频率是否正确
- 有寄存器的设计中,时钟 fanout 是否合理而非 0
- STA coverage 是否合理而非 0.00%
- 报告中是否存在该时钟域的寄存器路径
- 是否有 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 小版本的菜单名称可能略有不同,但对象关系相同:
- 新建工程,选择正确的 Family、Device 和封装
- 添加可综合
.v 源文件
- 将正确模块设置为 Top Module
- 建立或选择 Constraint Files/Constraint Set
- 添加
.adc
- 添加
.sdc
- 确认约束参与当前综合和 Physical Design run
- 重新运行综合,不复用过期结果
- 运行 Physical Design
- 打开最终引脚和时序报告
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
可简化理解为:
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}]
|
执行顺序
- 用
COUNT_MAX=3 完成自检仿真。
- TD 工程选择实际器件;本示例才使用
EG4S20BG256。
- 添加
led_top.v 并设 led_top 为顶层。
- 添加
top.adc、top.sdc 到当前约束文件集。
- 运行综合,检查顶层、警告和资源。
- 运行布局布线,检查最终引脚。
- 检查
sys_clk=50 MHz、fanout、coverage、setup 和 hold。
- 生成位流并下载。
- 检查复位和 LED 移动方向。
- 记录 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 个按键和 4 个 LED,先做映射表,再写
.adc。
- 分别写出 12、25、50、100 MHz 的
create_clock。
- 故意将
[get_ports {clk}] 改为不存在的端口,对比 warning、fanout 和 coverage,再恢复。
- 提交流水灯的 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 检查通过
- 位流时间戳与本次修改一致
- 板上现象与设计一致
第五阶段总复习
必须记住的六句话
- Testbench 要自动判断结果,不能只打印或只看波形。
- 时序输入在非采样沿驱动,在采样沿之后检查。
.adc 管引脚和 I/O 电气属性,.sdc 管时钟和接口时序。
- 顶层端口、原理图网络和封装引脚必须形成可核对的映射链。
WNS=0 不一定代表通过,还要检查 fanout、coverage 和未约束路径。
- 仿真、约束、实现、时序和上板全部闭环,工程才算完成。
推荐学习顺序
1
2
3
4
5
6
|
第 1 次:完成第 14 章自检式 Testbench
第 2 次:完成顶层端口到原理图的映射表
第 3 次:编写并检查 .adc
第 4 次:计算周期并编写 .sdc
第 5 次:建立 TD 工程并读综合、引脚和时序报告
第 6 次:完成流水灯上板和验证记录
|
不要一次复制所有文件后直接点击“生成位流”。每次解决一个闭环,才能知道错误位于哪一层。