一、实验背景
本次实验来源于数字集成电路课程实验,老师要求使用 Design Compiler 对 HDL 电路进行逻辑综合,并进一步使用 ModelSim/Questa 对综合后的门级网表进行时序仿真。
我上学期 FPGA 课程设计做的是一个以太网 UDP 数据报文相关工程,工程整体基于正点原子的以太网 UDP 示例进行修改。但完整工程中包含:
RGMII / GMII 接口转换;
ARP / ICMP / UDP 协议模块;
PLL;
异步 FIFO;
FPGA 厂商 IP;
多时钟域逻辑。
这些内容对初学者来说直接拿去做 ASIC 逻辑综合和门级后仿真难度比较高,也不太适合直接交给 Design Compiler 处理。
因此,本实验没有直接综合整个 FPGA 顶层工程,而是选取其中自己修改过的一个相对独立的小模块:udp_payload_append
该模块功能明确,且不依赖 FPGA 专用 IP,适合作为本次 DC 综合和后仿真的实验对象。


二、实验模块功能说明
1. 模块作用
该模块用于处理 UDP 接收到的 payload 数据。功能可以概括为:当接收到 UDP payload 时,将原始数据写入发送 FIFO;当一包数据接收完成后,自动追加固定字符串;追加完成后,产生发送启动信号,通知后级 UDP 发送模块开始发送。
2. 输入输出接口
主要输入信号:

主要输出信号:

3.与上板验证比较
这次 DC/ModelSim 仿真不是像 FPGA 上板那样真的发 UDP 包到电脑,而是在模块级别检查数字信号是否按预期变化。但这不是"没验证",而是换了一种更底层、更适合数字 IC 流程的验证方式。
3.1.上学期 FPGA 课设验证:系统级硬件验证 / 板级验证:
FPGA 下载 bit 文件
↓
网口发 UDP 包
↓
电脑网络调试助手收到
↓
直接看到 HELLO + 追加字符串
它验证的是完整链路:
你的逻辑
-
FIFO
-
UDP/IP 协议栈
-
GMII/RGMII
-
PHY 芯片
-
网线
-
电脑网卡
-
网络调试助手
直观看到:
HELLO
nihaoya,dongwazi~
3.2 DC 综合实验,选的是其中一个小模块:
udp_payload_append
这个模块本身不会真的发 UDP 包。它只负责:
收到 payload 数据
↓
写入 FIFO
↓
追加字符串
↓
告诉后级 udp_tx 可以发送
所以我们仿真时检查的是它的接口信号:
wr_fifo_en
wr_fifo_data
tx_byte_num
tx_start_en
des_mac
des_ip
也就是说:本实验验证的是"这个模块有没有正确把 HELLO 和追加字符串写到 FIFO 接口上",而不是验证"电脑网络调试助手能不能收到 UDP 包"。
4. 追加字符串
模块中固定追加的字符串为:\nnihaoya,dongwazi~,长度为:18 字节
假设原始输入 payload 是:HELLO,长度为 5 字节,
则模块最终写入 FIFO 的内容为:HELLO\nnihaoya,dongwazi~,总长度为:5 + 18 = 23 字节
因此仿真时重点检查:tx_byte_num = 23,wr_fifo_data 共写出 23 个字节,tx_start_en 最后产生一个时钟周期脉冲, 捕获到的字符串Captured string: HELLO\nnihaoya,dongwazi~
三、实验环境
1.本次实验使用的环境如下:


2.涉及的基本 Linux 命令总结
- cd
切换目录
cd ~/dc_udp_payload_lab
常见用法:
cd ~ # 回到家目录
cd .. # 返回上一级目录
cd /path # 进入指定绝对路径
- ls
查看目录内容。
ls -lh
以详细、人类易读形式显示文件信息。
例如:
ls -lh netlist
查看 netlist 目录中的文件。
- mkdir -p
创建目录。
mkdir -p dc_udp_payload_lab/{rtl,sim,constraint,dc,netlist,report,lib}
含义:一次性创建多个目录。
其中:-p表示如果上级目录不存在,就自动创建;如果目录已经存在,也不会报错。
- find
查找文件。
find ~/dc_udp_payload_lab/lib/pdk_tsmc18 -type f -name "*.v" | head
含义:在 pdk_tsmc18 目录下查找所有 .v 文件,并显示前几行。
常见用法:
find ~ -type f -name "typical.db"
用于查找某个文件的位置。
- cp -r
复制目录。
例如:把 pdk_tsmc18 目录复制到工程的 lib 目录下
找到库:find ~ -type f -name typical.db 2>/dev/null,找到以后复制库
假设刚才输出的是:/home/ICer/Desktop/dcLab/dcLab/lib/pdk_tsmc18/SynopsysDC/typical.db
cp -r /home/ICer/Desktop/dcLab/dcLab/lib/pdk_tsmc18 ~/dc_udp_payload_lab/lib/
- grep
在文本中搜索关键词。
grep -i "slack" report/timing.rpt | head
含义:
在 timing.rpt 中搜索 slack,不区分大小写,只显示前几行。
其中:-i表示不区分大小写。
例如:grep -i "violated" report/constraint.rpt
用于查找是否有约束违例。
- head
显示文件或输出的前几行。
head report/timing.rpt
也常和管道一起用:grep -i "slack" report/timing.rpt | head
- tee
一边显示输出,一边保存日志。
dc_shell-t -f run_dc.tcl | tee ../report/dc_run.log
含义:运行 DC 综合,同时把终端输出保存到 dc_run.log。
后仿真中也用到:vsim -c -do run_gate_batch.do | tee gate_run.log
这样可以保留仿真日志,方便之后写报告。
- cat > file <<'EOF'
在终端中创建文件。
例如:cat > dc/run_dc.tcl <<'EOF'
...
EOF
含义:把 EOF 中间的内容写入 dc/run_dc.tcl 文件。
这种方式适合快速生成脚本文件。
- command -v
检查某个命令是否存在。
command -v dc_shell-t
command -v vsim
command -v vlog
如果有输出路径,说明该工具已经安装或环境变量已经配置好。
- pwd
显示当前所在目录。
例如输出:/home/ICer/dc_udp_payload_lab
四、工程目录结构
最终工程目录为:
~/dc_udp_payload_lab/
├── rtl/
│ └── udp_payload_append.v
├── constraint/
│ └── udp_payload_append.con
├── dc/
│ └── run_dc.tcl
├── sim/
│ ├── tb_udp_payload_append.v
│ ├── run_rtl.do
│ ├── run_gate.do
│ ├── run_gate_batch.do
│ └── gate_run.log
├── netlist/
│ ├── udp_payload_append_syn.v
│ ├── udp_payload_append.sdf
│ ├── udp_payload_append.sdc
│ └── udp_payload_append.ddc
├── report/
│ ├── area.rpt
│ ├── timing.rpt
│ ├── qor.rpt
│ ├── resources.rpt
│ ├── constraint.rpt
│ ├── check_design.rpt
│ └── dc_run.log
└── lib/
└── pdk_tsmc18/
├── SynopsysDC/
│ ├── typical.db
│ ├── fast.db
│ └── slow.db
└── Verilog/
├── tsmc18.v
├── tsmc18_neg.v
└── tpz973g.v
五、实验整体流程
本实验整体流程如下:
准备 RTL 源码
↓
准备标准单元库
↓
编写约束文件
↓
编写 DC 综合脚本
↓
运行 Design Compiler 综合
↓
生成门级网表、SDF、报告
↓
使用 ModelSim 做 RTL 前仿真
↓
使用 ModelSim 做门级后仿真
↓
检查 SDF 反标和仿真结果
六.实验
第 1 步:创建工程目录并复制工艺库
在终端中创建工程目录:
cd ~
mkdir -p dc_udp_payload_lab/rtl
mkdir -p dc_udp_payload_lab/sim
mkdir -p dc_udp_payload_lab/constraint
mkdir -p dc_udp_payload_lab/dc
mkdir -p dc_udp_payload_lab/netlist
mkdir -p dc_udp_payload_lab/report
mkdir -p dc_udp_payload_lab/lib

复制标准单元库:

cp -r /path/to/pdk_tsmc18 ~/dc_udp_payload_lab/lib/
检查 DC 综合库是否存在:
ls ~/dc_udp_payload_lab/lib/pdk_tsmc18/SynopsysDC
看到:fast.db slow.db typical.db 就说明库放好了。

其中本实验主要使用:typical.db
第 2 步:创建 RTL 代码文件
进入你的工程目录,创建 RTL 代码文件
cd ~/dc_udp_payload_lab
创建 RTL 代码文件:

cat > rtl/udp_payload_append.v <<'EOF'
`timescale 1ns/1ps
module udp_payload_append (
input wire clk,
input wire rst_n,
input wire rec_en,
input wire [7:0] rec_data,
input wire rec_pkt_done,
input wire [15:0] rec_byte_num,
input wire [47:0] src_mac,
input wire [31:0] src_ip,
output reg wr_fifo_en,
output reg [7:0] wr_fifo_data,
output reg tx_start_en,
output reg [15:0] tx_byte_num,
output reg [47:0] des_mac,
output reg [31:0] des_ip
);
localparam [15:0] APPEND_LEN_16 = 16'd18;
localparam [5:0] APPEND_LAST = 6'd17;
function [7:0] append_byte;
input [5:0] idx;
begin
case (idx)
6'd0 : append_byte = 8'h0A;
6'd1 : append_byte = 8'h6E;
6'd2 : append_byte = 8'h69;
6'd3 : append_byte = 8'h68;
6'd4 : append_byte = 8'h61;
6'd5 : append_byte = 8'h6F;
6'd6 : append_byte = 8'h79;
6'd7 : append_byte = 8'h61;
6'd8 : append_byte = 8'h2C;
6'd9 : append_byte = 8'h64;
6'd10: append_byte = 8'h6F;
6'd11: append_byte = 8'h6E;
6'd12: append_byte = 8'h67;
6'd13: append_byte = 8'h77;
6'd14: append_byte = 8'h61;
6'd15: append_byte = 8'h7A;
6'd16: append_byte = 8'h69;
6'd17: append_byte = 8'h7E;
default: append_byte = 8'h00;
endcase
end
endfunction
reg appending;
reg [5:0] append_idx;
reg start_pending;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
wr_fifo_en <= 1'b0;
wr_fifo_data <= 8'd0;
tx_start_en <= 1'b0;
tx_byte_num <= 16'd0;
des_mac <= 48'd0;
des_ip <= 32'd0;
appending <= 1'b0;
append_idx <= 6'd0;
start_pending <= 1'b0;
end else begin
wr_fifo_en <= 1'b0;
tx_start_en <= 1'b0;
if(rec_en) begin
wr_fifo_en <= 1'b1;
wr_fifo_data <= rec_data;
end
if(rec_pkt_done) begin
des_mac <= src_mac;
des_ip <= src_ip;
tx_byte_num <= rec_byte_num + APPEND_LEN_16;
appending <= 1'b1;
append_idx <= 6'd0;
start_pending <= 1'b0;
end
else if(appending) begin
wr_fifo_en <= 1'b1;
wr_fifo_data <= append_byte(append_idx);
if(append_idx == APPEND_LAST) begin
appending <= 1'b0;
append_idx <= 6'd0;
start_pending <= 1'b1;
end else begin
append_idx <= append_idx + 6'd1;
end
end
if(start_pending) begin
start_pending <= 1'b0;
tx_start_en <= 1'b1;
end
end
end
endmodule
EOF
然后检查一下:
ls -lh rtl
如果看到:udp_payload_append.v 就对了。

第 3 步:创建约束文件
cat > constraint/udp_payload_append.con <<'EOF'
set CLK_PERIOD 10.0
create_clock -name clk -period $CLK_PERIOD [get_ports clk]
set_clock_uncertainty 0.2 [get_clocks clk]
set_false_path -from [get_ports rst_n]
set_input_delay 1.0 -clock [get_clocks clk] \
[remove_from_collection [all_inputs] [get_ports {clk rst_n}]]
set_output_delay 1.0 -clock [get_clocks clk] [all_outputs]
set_drive 0 [get_ports clk]
set_load 0.05 [all_outputs]
set_max_fanout 20 [current_design]
EOF

约束文件:
- 设置时钟周期
set CLK_PERIOD 10.0
表示:clk 的周期为 10ns,对应频率为:100MHz
- 设置时钟不确定性
set_clock_uncertainty 0.2 [get_clocks clk],表示为时钟留出 0.2ns 的裕量。
实际芯片中时钟存在抖动和偏差,综合时需要预留一定余量。
- 设置复位为 false path
set_false_path -from [get_ports rst_n]
rst_n 是异步复位信号,不参与普通数据路径时序分析,因此设置为 false path。
- 设置输入延时和输出延时
set_input_delay 1.0 -clock [get_clocks clk] ...
set_output_delay 1.0 -clock [get_clocks clk] [all_outputs]
表示输入输出信号相对时钟留出 1ns 的延时约束。
- 设置输出负载
set_load 0.05 [all_outputs]
表示输出端口带有一定负载,用于更接近实际电路情况。
第 4 步:创建 DC 综合脚本
cat > dc/run_dc.tcl <<'EOF'
set DESIGN_NAME udp_payload_append
set DC_DIR [pwd]
set ROOT_DIR [file normalize "$DC_DIR/.."]
set RTL_DIR "$ROOT_DIR/rtl"
set CON_DIR "$ROOT_DIR/constraint"
set NET_DIR "$ROOT_DIR/netlist"
set RPT_DIR "$ROOT_DIR/report"
set LIB_DIR "$ROOT_DIR/lib/pdk_tsmc18/SynopsysDC"
file mkdir $NET_DIR
file mkdir $RPT_DIR
file mkdir "$DC_DIR/work"
if {![file exists "$LIB_DIR/typical.db"]} {
puts "ERROR: Cannot find typical.db!"
puts "$LIB_DIR/typical.db"
quit
}
set_app_var search_path [list . $RTL_DIR $LIB_DIR]
set_app_var target_library [list typical.db]
set_app_var link_library [list * typical.db]
define_design_lib WORK -path "$DC_DIR/work"
analyze -format verilog "$RTL_DIR/udp_payload_append.v"
elaborate $DESIGN_NAME
current_design $DESIGN_NAME
link
uniquify
redirect -file "$RPT_DIR/check_design.rpt" {
check_design
}
source "$CON_DIR/udp_payload_append.con"
redirect -file "$RPT_DIR/check_timing_before_compile.rpt" {
check_timing
}
compile -map_effort medium -area_effort medium
redirect -file "$RPT_DIR/area.rpt" {
report_area -hierarchy
}
redirect -file "$RPT_DIR/timing.rpt" {
report_timing -delay max -max_paths 10
}
redirect -file "$RPT_DIR/qor.rpt" {
report_qor
}
redirect -file "$RPT_DIR/resources.rpt" {
report_reference
}
redirect -file "$RPT_DIR/constraint.rpt" {
report_constraint -all_violators
}
redirect -file "$RPT_DIR/check_timing_after_compile.rpt" {
check_timing
}
change_names -rules verilog -hierarchy
write -format verilog -hierarchy -output "$NET_DIR/${DESIGN_NAME}_syn.v"
write_sdf "$NET_DIR/${DESIGN_NAME}.sdf"
write_sdc "$NET_DIR/${DESIGN_NAME}.sdc"
write -format ddc -hierarchy -output "$NET_DIR/${DESIGN_NAME}.ddc"
puts "============================================================"
puts "DC synthesis finished!"
puts "============================================================"
puts "Netlist files are in:"
puts "$NET_DIR"
puts "Report files are in:"
puts "$RPT_DIR"
puts "============================================================"
quit
EOF
检查:ls -lh dc
看到:run_dc.tcl 就对。

4.1 run_dc.tcl 的作用
run_dc.tcl 是 Design Compiler 的自动化综合脚本。
它的作用是:告诉 DC:用哪个工艺库,读哪个 RTL,加载什么约束,怎么综合,最后把网表和报告输出到哪里。
概括为:
设置路径
↓
设置标准单元库
↓
读入 RTL
↓
展开顶层模块
↓
连接设计和库
↓
加载约束
↓
检查设计和时序
↓
执行综合
↓
输出报告和网表
4.2. run_dc.tcl 脚本流程说明
第一部分:设置设计名和路径
set DESIGN_NAME udp_payload_append
set DC_DIR [pwd]
set ROOT_DIR [file normalize "$DC_DIR/.."]
set RTL_DIR "$ROOT_DIR/rtl"
set CON_DIR "$ROOT_DIR/constraint"
set NET_DIR "$ROOT_DIR/netlist"
set RPT_DIR "$ROOT_DIR/report"
set LIB_DIR "$ROOT_DIR/lib/pdk_tsmc18/SynopsysDC"
这部分主要是定义变量。

例如:set DESIGN_NAME udp_payload_append 表示本次综合的顶层模块叫udp_payload_append
第二部分:创建输出目录
file mkdir $NET_DIR
file mkdir $RPT_DIR
file mkdir "$DC_DIR/work"
这部分用于创建:netlist/,report/,dc/work/ ,防止后面输出文件时目录不存在。
第三部分:检查 typical.db 是否存在
如果 DC 找不到:typical.db就停止运行。因为没有 .db 工艺库,DC 不知道要把 RTL 映射成哪些标准单元。
第四部分:设置搜索路径和工艺库
set_app_var search_path [list . RTL_DIR LIB_DIR]
告诉 DC:查找文件时,到当前目录、RTL 目录、库目录中查找。
set_app_var target_library [list typical.db]
target_library 是综合目标库。DC 最终把 RTL 映射成 typical.db 里面的标准单元,比如:INV,NAND,NOR
set_app_var link_library [list * typical.db]
link_library 是链接库。DC 在解析模块和标准单元定义时,到当前设计和 typical.db 中查找。其中 * 表示当前已经读入的设计。
简记:target_library:用哪些标准单元造电路,link_library:去哪找模块和单元定义
第五部分:定义 DC 工作库
给 DC 准备一个临时工作目录。DC 在分析和展开设计时,会把内部中间文件放在这里。
第六部分:读入 RTL
analyze -format verilog "$RTL_DIR/udp_payload_append.v"
analyze 的作用是:读取 Verilog 文件,并检查语法。
第七部分:展开顶层设计
elaborate $DESIGN_NAME
current_design $DESIGN_NAME
elaborate 的作用是:根据顶层模块名,把 RTL 展开成 DC 内部能理解的电路结构。current_design 的作用是:告诉 DC 当前要处理的设计是 udp_payload_append。
第八部分:链接设计
link
uniquify
link 的作用是:把设计中引用的模块和工艺库连接起来,检查有没有找不到的模块或单元。
uniquify 的作用是:本实验模块比较简单,uniquify 主要是规范流程。
第九部分:检查设计
check_design 用于检查设计结构是否存在问题,例如:未连接端口;找不到模块;不合理的逻辑结构。
第十部分:加载时序约束
加载约束文件:constraint/udp_payload_append.con,没有约束的话,DC 不知道你的电路需要跑多快。
第十一部分:综合前时序检查
check_timing 用于检查时序约束是否完整。比如:有没有创建时钟;有没有无约束路径;输入输出延时是否合理。
第十二部分:执行综合
compile -map_effort medium -area_effort medium
compile 会把 RTL 转换成标准单元门级网表。
也就是:
Verilog RTL
↓
逻辑优化
↓
映射到 typical.db 中的标准单元
↓
生成门级电路
map_effort medium 表示映射优化强度为中等。area_effort medium 表示面积优化强度为中等。
第十三部分:生成报告

第十四部分:检查综合后时序
综合完成后再次检查时序约束,确认设计仍然满足时序检查要求。
第十五部分:修改命名规则
DC 综合后有些内部信号名可能包含 Verilog 不友好的字符。这一步用于把信号名转换成合法 Verilog 名称,方便后续仿真。
第十六部分:输出网表、SDF、SDC、DDC
输出结果包括:

第十七部分:结束 DC
puts "DC synthesis finished!"
quit
puts 用来打印提示信息。quit 用来退出 DC。看到:DC synthesis finished!表示综合流程正常跑完。
综上,设路径 → 设库 → 读 RTL → 展开顶层 → 连接库 → 加约束 → 检查 → 综合 → 出报告 → 出网表
第 5 步:跑 DC 综合
进入 DC 脚本目录:
cd ~/dc_udp_payload_lab/dc
运行综合:
dc_shell-t -f run_dc.tcl | tee ../report/dc_run.log
其中:

成功后终端会出现:DC synthesis finished!


第 6 步:检查综合结果
回到工程目录:
cd ~/dc_udp_payload_lab
查看综合网表文件:
ls -lh netlist
可以看到:
udp_payload_append_syn.v
udp_payload_append.sdf
udp_payload_append.sdc
udp_payload_append.ddc
查看报告文件:
ls -lh report
可以看到:
area.rpt
timing.rpt
qor.rpt
constraint.rpt
检查时序
grep -i "slack" report/timing.rpt | head
结果中出现:slack (MET),并且 slack 为正数,例如:slack (MET) 6.47 ,说明时序满足。
检查约束违例
grep -i "violated" report/constraint.rpt
结果为:There are no violated constraints.说明没有违反约束。

第 7 步:RTL 前仿真
1. RTL 前仿真
RTL 仿真是直接仿真源代码:rtl/udp_payload_append.v
作用是验证:自己写的 Verilog 逻辑是否正确。
2. 创建 testbench 文件
测试内容:
模拟输入 5 字节数据:HELLO
检查模块是否追加字符串;
检查输出长度是否为 23;
检查 tx_start_en 是否产生;
检查 FIFO 写出数据数量是否正确。
进入工程目录:
cd ~/dc_udp_payload_lab
创建 testbench 文件:
cat > sim/tb_udp_payload_append.v <<'EOF'
`timescale 1ns/1ps
module tb_udp_payload_append;
reg clk;
reg rst_n;
reg rec_en;
reg [7:0] rec_data;
reg rec_pkt_done;
reg [15:0] rec_byte_num;
reg [47:0] src_mac;
reg [31:0] src_ip;
wire wr_fifo_en;
wire [7:0] wr_fifo_data;
wire tx_start_en;
wire [15:0] tx_byte_num;
wire [47:0] des_mac;
wire [31:0] des_ip;
integer i;
integer errors;
integer wr_count;
reg [7:0] captured [0:63];
//============================================================
// 实例化待测模块 DUT
//============================================================
udp_payload_append dut (
.clk (clk),
.rst_n (rst_n),
.rec_en (rec_en),
.rec_data (rec_data),
.rec_pkt_done (rec_pkt_done),
.rec_byte_num (rec_byte_num),
.src_mac (src_mac),
.src_ip (src_ip),
.wr_fifo_en (wr_fifo_en),
.wr_fifo_data (wr_fifo_data),
.tx_start_en (tx_start_en),
.tx_byte_num (tx_byte_num),
.des_mac (des_mac),
.des_ip (des_ip)
);
//============================================================
// 产生 100MHz 时钟,周期 10ns
//============================================================
initial begin
clk = 1'b0;
forever #5 clk = ~clk;
end
//============================================================
// 超时保护,防止仿真卡死
//============================================================
initial begin
#5000;
$display("ERROR: Simulation timeout!");
$finish;
end
//============================================================
// 在时钟下降沿采集 FIFO 写出的数据
//============================================================
always @(negedge clk) begin
if(!rst_n) begin
wr_count = 0;
end
else begin
if(wr_fifo_en) begin
captured[wr_count] = wr_fifo_data;
$display("%t FIFO write[%0d] = 0x%02h", $time, wr_count, wr_fifo_data);
wr_count = wr_count + 1;
end
end
end
//============================================================
// 期望输出字节:
// HELLO + "\nnihaoya,dongwazi~"
//============================================================
function [7:0] expected_byte;
input integer idx;
begin
case(idx)
0 : expected_byte = 8'h48; // H
1 : expected_byte = 8'h45; // E
2 : expected_byte = 8'h4C; // L
3 : expected_byte = 8'h4C; // L
4 : expected_byte = 8'h4F; // O
5 : expected_byte = 8'h0A; // \n
6 : expected_byte = 8'h6E; // n
7 : expected_byte = 8'h69; // i
8 : expected_byte = 8'h68; // h
9 : expected_byte = 8'h61; // a
10: expected_byte = 8'h6F; // o
11: expected_byte = 8'h79; // y
12: expected_byte = 8'h61; // a
13: expected_byte = 8'h2C; // ,
14: expected_byte = 8'h64; // d
15: expected_byte = 8'h6F; // o
16: expected_byte = 8'h6E; // n
17: expected_byte = 8'h67; // g
18: expected_byte = 8'h77; // w
19: expected_byte = 8'h61; // a
20: expected_byte = 8'h7A; // z
21: expected_byte = 8'h69; // i
22: expected_byte = 8'h7E; // ~
default: expected_byte = 8'h00;
endcase
end
endfunction
//============================================================
// 发送一个输入字节给 DUT
//============================================================
task send_byte;
input [7:0] data;
input done;
begin
@(negedge clk);
rec_en = 1'b1;
rec_data = data;
rec_pkt_done = done;
if(done)
rec_byte_num = 16'd5;
else
rec_byte_num = 16'd0;
end
endtask
//============================================================
// 主测试流程
//============================================================
initial begin
errors = 0;
wr_count = 0;
rst_n = 1'b0;
rec_en = 1'b0;
rec_data = 8'd0;
rec_pkt_done = 1'b0;
rec_byte_num = 16'd0;
src_mac = 48'hAA_BB_CC_DD_EE_FF;
src_ip = {8'd192, 8'd168, 8'd1, 8'd102};
// 复位保持 5 个周期
repeat(5) @(negedge clk);
rst_n = 1'b1;
$display("INFO: reset released.");
// 输入 5 字节 payload:HELLO
send_byte(8'h48, 1'b0); // H
send_byte(8'h45, 1'b0); // E
send_byte(8'h4C, 1'b0); // L
send_byte(8'h4C, 1'b0); // L
send_byte(8'h4F, 1'b1); // O,同时表示一包结束
// 拉低输入信号
@(negedge clk);
rec_en = 1'b0;
rec_data = 8'd0;
rec_pkt_done = 1'b0;
rec_byte_num = 16'd0;
// 等待发送启动信号产生
wait(tx_start_en == 1'b1);
#1;
$display("INFO: tx_start_en detected.");
//==========================================================
// 检查 tx_byte_num
//==========================================================
if(tx_byte_num !== 16'd23) begin
$display("ERROR: tx_byte_num = %0d, expected 23", tx_byte_num);
errors = errors + 1;
end
else begin
$display("PASS: tx_byte_num = 23");
end
//==========================================================
// 检查目的 MAC
//==========================================================
if(des_mac !== src_mac) begin
$display("ERROR: des_mac mismatch!");
errors = errors + 1;
end
else begin
$display("PASS: des_mac correct.");
end
//==========================================================
// 检查目的 IP
//==========================================================
if(des_ip !== src_ip) begin
$display("ERROR: des_ip mismatch!");
errors = errors + 1;
end
else begin
$display("PASS: des_ip correct.");
end
//==========================================================
// 检查 FIFO 写入字节数
//==========================================================
if(wr_count !== 23) begin
$display("ERROR: wr_count = %0d, expected 23", wr_count);
errors = errors + 1;
end
else begin
$display("PASS: wr_count = 23");
end
//==========================================================
// 逐字节检查 FIFO 写入内容
//==========================================================
for(i = 0; i < 23; i = i + 1) begin
if(captured[i] !== expected_byte(i)) begin
$display("ERROR: captured[%0d] = 0x%02h, expected 0x%02h",
i, captured[i], expected_byte(i));
errors = errors + 1;
end
end
//==========================================================
// 打印捕获到的字符串,方便人眼查看
// 这里把换行符 0x0A 打印成可见的 \n
//==========================================================
$write("Captured string: ");
for(i = 0; i < 23; i = i + 1) begin
if(captured[i] == 8'h0A)
$write("\\n");
else
$write("%c", captured[i]);
end
$write("\n");
//==========================================================
// 输出最终测试结果
//==========================================================
if(errors == 0) begin
$display("========================================");
$display("TEST PASSED!");
$display("========================================");
end
else begin
$display("========================================");
$display("TEST FAILED! errors = %0d", errors);
$display("========================================");
end
#50;
$finish;
end
endmodule
EOF
3. 创建 RTL 仿真脚本

cat > sim/run_rtl.do <<'EOF'
if {[file exists work]} {
vdel -lib work -all
}
vlib work
vmap work work
vlog -work work ../rtl/udp_payload_append.v
vlog -work work tb_udp_payload_append.v
vsim -voptargs=+acc work.tb_udp_payload_append
add wave -radix binary /tb_udp_payload_append/clk
add wave -radix binary /tb_udp_payload_append/rst_n
add wave -radix binary /tb_udp_payload_append/rec_en
add wave -radix hexadecimal /tb_udp_payload_append/rec_data
add wave -radix binary /tb_udp_payload_append/rec_pkt_done
add wave -radix unsigned /tb_udp_payload_append/rec_byte_num
add wave -radix binary /tb_udp_payload_append/wr_fifo_en
add wave -radix hexadecimal /tb_udp_payload_append/wr_fifo_data
add wave -radix binary /tb_udp_payload_append/tx_start_en
add wave -radix unsigned /tb_udp_payload_append/tx_byte_num
add wave -radix hexadecimal /tb_udp_payload_append/des_mac
add wave -radix hexadecimal /tb_udp_payload_append/des_ip
run -all
EOF
4. 运行 RTL 仿真
cd ~/dc_udp_payload_lab/sim
vsim -do run_rtl.do

仿真通过后可以看到:
打印字符串:Captured string: HELLO\nnihaoya,dongwazi~
PASS: tx_byte_num = 23
PASS: des_mac correct.
PASS: des_ip correct.
PASS: wr_count = 23
TEST PASSED!
说明 RTL 功能仿真通过。

第 8 步:门级后仿真
- 后仿真也叫门级仿真。
它仿真的不是 RTL 源码,而是 DC 综合后生成的门级网表,并且加载 SDF 延时文件,所以后仿真验证的是:综合后的门级电路,在带延时的情况下,功能是否仍然正确。
- RTL 仿真和后仿真的区别

- 检查标准单元 Verilog 模型
后仿真需要标准单元 Verilog 模型。
检查命令:find ~/dc_udp_payload_lab/lib/pdk_tsmc18 -type f -name "*.v" | head
结果:
/home/ICer/dc_udp_payload_lab/lib/pdk_tsmc18/Verilog/tpz973g.v
/home/ICer/dc_udp_payload_lab/lib/pdk_tsmc18/Verilog/tsmc18.v
/home/ICer/dc_udp_payload_lab/lib/pdk_tsmc18/Verilog/tsmc18_neg.v
本实验使用:tsmc18.v
- 为什么后仿真需要 tsmc18.v?
综合后的网表中已经不是 RTL 语句,而是标准单元实例,例如:DFF,NAND,NOR,INV,ModelSim 不认识这些标准单元,因此需要:tsmc18.v来告诉 ModelSim 每个标准单元的仿真行为。
- 创建门级后仿真脚本
脚本文件:sim/run_gate_batch.do
Linux 终端,复制:
cat > sim/run_gate.do <<'EOF'
catch {quit -sim}
if {[file exists work]} {
vdel -lib work -all
}
vlib work
vmap work work
vlog -work work ../lib/pdk_tsmc18/Verilog/tsmc18.v
vlog -work work ../netlist/udp_payload_append_syn.v
vlog -work work tb_udp_payload_append.v
vsim -voptargs=+acc -sdftyp /tb_udp_payload_append/dut=../netlist/udp_payload_append.sdf work.tb_udp_payload_append
add wave -radix binary /tb_udp_payload_append/clk
add wave -radix binary /tb_udp_payload_append/rst_n
add wave -radix binary /tb_udp_payload_append/rec_en
add wave -radix hexadecimal /tb_udp_payload_append/rec_data
add wave -radix binary /tb_udp_payload_append/rec_pkt_done
add wave -radix unsigned /tb_udp_payload_append/rec_byte_num
add wave -radix binary /tb_udp_payload_append/wr_fifo_en
add wave -radix hexadecimal /tb_udp_payload_append/wr_fifo_data
add wave -radix binary /tb_udp_payload_append/tx_start_en
add wave -radix unsigned /tb_udp_payload_append/tx_byte_num
add wave -radix hexadecimal /tb_udp_payload_append/des_mac
add wave -radix hexadecimal /tb_udp_payload_append/des_ip
run -all
EOF
其中关键部分是:-sdftyp /tb_udp_payload_append/dut=../netlist/udp_payload_append.sdf
意思是:把 SDF 延时文件反标到 testbench 中名为 dut 的被测模块实例上。
- 运行门级后仿真
cd ~/dc_udp_payload_lab/sim
vsim -c -do run_gate_batch.do | tee gate_run.log
这里使用:vsim -c ,表示命令行模式运行,不打开图形界面。这样更稳定,也方便保存日志。


说明:TEST PASSED!打印
在 testbench 里还有一个逐字节比较逻辑:
for(i = 0; i < 23; i = i + 1) begin
if(captured[i] !== expected_byte(i)) begin
$display("ERROR...");
errors = errors + 1;
end
end
testbench 把模块写出的 23 个字节全部存起来,然后和期望值逐个比较。如果其中任何一个字节错了,都会打印 ERROR,最后不会出现 TEST PASSED!。
所以:TEST PASSED!的含义其实是:23 个字节内容也全部逐字节对。
- 检查后仿真结果
查看日志:
grep -E "SDF Backannotation|TEST PASSED|Errors:" gate_run.log
结果包含:
SDF Backannotation Successfully Completed.
TEST PASSED!
Errors: 0, Warnings: 1
说明:SDF 延时文件成功反标;门级网表仿真通过;没有 error;warning 来自标准单元 timing check,不影响功能结果。

七.补充思考:从串并转换实验理解门级后仿中的"中间态"
在完成本次 udp_payload_append 模块的 DC 综合和门级后仿后,我又接触了另一个小实验:serial_to_parallel_16bit,即 16 位串转并加七段数码管显示。这个实验虽然和本次 UDP payload 追加模块不是同一个设计,但同样经历了:
RTL 仿真
↓
DC 综合
↓
门级网表 + SDF 后仿真

在观察这个串转并模块的波形时,我发现一个很有意思的现象:门级后仿波形和 RTL 仿真波形相比,并不只是简单地整体延迟一点点,而是在某些总线信号上会出现短暂的中间值。例如最终结果应为:pdata = 16'h2C42
七段数码管最终显示也正确,对应:2 C 4 2
但是在门级后仿波形中,pdata 在稳定为 16'h2C42 之前,可能会短暂出现类似:16'h0440
16'h0C40这样的过渡值。
1.RTL仿真

2.门级后仿

- 串转并结构本身就存在逐位移入的中间状态
串转并模块的核心逻辑一般类似:shift_reg <= {shift_reg[14:0], sdata};
也就是每来一个有效时钟,就把当前移位寄存器左移一位,并把新的串行输入 sdata 放到最低位。如果输入的 16 位串行数据是:0010_1100_0100_0010,也就是:16'h2C42
那么内部移位寄存器并不是一开始就等于 2C42,而是随着每个时钟周期逐步累积数据。也就是说,shift_reg 本来就会经历很多中间状态。所以对于串转并电路来说,观察内部移位寄存器时,看到数据逐步变化是正常的。
2.RTL仿真
if(cnt == 4'd15) begin
shift_reg <= {shift_reg[14:0], sdata};
pdata <= {shift_reg[14:0], sdata};
cnt <= 4'd0;
pin <= 1'b1;
end
也就是说:shift_reg 是每个有效时钟都在移位;但是 pdata 只有在收到第 16 位时才更新成完整的 16 位并行数据。所以理想 RTL 仿真里,pdata 大概率是:0000 → 2C42 直接跳到最终值。
3.门级后仿
这个就是门级后仿和 RTL 仿真的关键区别。
RTL 仿真是理想模型
RTL 仿真里,类似:pdata <= 16'h2C42;看起来好像 16 位同时更新。所以波形里可能表现为:
pdata: 0000 直接跳到 2C42
门级后仿是真实门延时模型
综合后,pdata[15:0] 不再是一个抽象的 16 位变量,而是 16 个触发器输出:
pdata[15] 一个 DFF
pdata[14] 一个 DFF
pdata[13] 一个 DFF
...
pdata[0] 一个 DFF
这些触发器经过标准单元库和 SDF 延时后,每一位的时钟到输出延时不完全一样。也就是说,在同一个时钟边沿之后:
pdata[10] 可能先变
pdata[6] 可能再变
pdata[11] 可能后变
pdata[13] 可能更后变
pdata[1] 最后变
于是 ModelSim 把 16 位总线合起来显示时,就会看到短暂的中间值,比如:
0000
↓
0440
↓
0C40
↓
2C42
这不是功能错误。它表示:16 位总线的各个位正在陆续稳定下来。
因此,门级后仿波形不一定要和 RTL 波形长得完全一样。
