基于 Design Compiler 的 UDP Payload 追加控制模块综合与门级后仿真

一、实验背景

本次实验来源于数字集成电路课程实验,老师要求使用 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 命令总结

  1. cd

切换目录

cd ~/dc_udp_payload_lab

常见用法:

cd ~ # 回到家目录

cd .. # 返回上一级目录

cd /path # 进入指定绝对路径

  1. ls

查看目录内容。

ls -lh

以详细、人类易读形式显示文件信息。

例如:

ls -lh netlist

查看 netlist 目录中的文件。

  1. mkdir -p

创建目录。

mkdir -p dc_udp_payload_lab/{rtl,sim,constraint,dc,netlist,report,lib}

含义:一次性创建多个目录。

其中:-p表示如果上级目录不存在,就自动创建;如果目录已经存在,也不会报错。

  1. find

查找文件。

find ~/dc_udp_payload_lab/lib/pdk_tsmc18 -type f -name "*.v" | head

含义:在 pdk_tsmc18 目录下查找所有 .v 文件,并显示前几行。

常见用法:

find ~ -type f -name "typical.db"

用于查找某个文件的位置。

  1. 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/

  1. grep

在文本中搜索关键词。

grep -i "slack" report/timing.rpt | head

含义:

在 timing.rpt 中搜索 slack,不区分大小写,只显示前几行。

其中:-i表示不区分大小写。

例如:grep -i "violated" report/constraint.rpt

用于查找是否有约束违例。

  1. head

显示文件或输出的前几行。

head report/timing.rpt

也常和管道一起用:grep -i "slack" report/timing.rpt | head

  1. 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

这样可以保留仿真日志,方便之后写报告。

  1. cat > file <<'EOF'

在终端中创建文件。

例如:cat > dc/run_dc.tcl <<'EOF'

...

EOF

含义:把 EOF 中间的内容写入 dc/run_dc.tcl 文件。

这种方式适合快速生成脚本文件。

  1. command -v

检查某个命令是否存在。

command -v dc_shell-t

command -v vsim

command -v vlog

如果有输出路径,说明该工具已经安装或环境变量已经配置好。

  1. 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

约束文件:

  1. 设置时钟周期

set CLK_PERIOD 10.0

表示:clk 的周期为 10ns,对应频率为:100MHz

  1. 设置时钟不确定性

set_clock_uncertainty 0.2 [get_clocks clk],表示为时钟留出 0.2ns 的裕量。

实际芯片中时钟存在抖动和偏差,综合时需要预留一定余量。

  1. 设置复位为 false path

set_false_path -from [get_ports rst_n]

rst_n 是异步复位信号,不参与普通数据路径时序分析,因此设置为 false path。

  1. 设置输入延时和输出延时

set_input_delay 1.0 -clock [get_clocks clk] ...

set_output_delay 1.0 -clock [get_clocks clk] [all_outputs]

表示输入输出信号相对时钟留出 1ns 的延时约束。

  1. 设置输出负载

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 步:门级后仿真

  1. 后仿真也叫门级仿真。

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

  1. RTL 仿真和后仿真的区别
  1. 检查标准单元 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

  1. 为什么后仿真需要 tsmc18.v?

综合后的网表中已经不是 RTL 语句,而是标准单元实例,例如:DFF,NAND,NOR,INV,ModelSim 不认识这些标准单元,因此需要:tsmc18.v来告诉 ModelSim 每个标准单元的仿真行为。

  1. 创建门级后仿真脚本

脚本文件: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 的被测模块实例上。

  1. 运行门级后仿真

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 个字节内容也全部逐字节对。

  1. 检查后仿真结果

查看日志:

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.门级后仿

  1. 串转并结构本身就存在逐位移入的中间状态

串转并模块的核心逻辑一般类似: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 波形长得完全一样。

相关推荐
时空自由民.7 小时前
蓝牙GAP/GATT协议和计算机网络TCP/UDP通信对比
tcp/ip·计算机网络·udp
之歆7 小时前
Day05_CSS完整博客笔记(下)
前端·css·笔记
泽克7 小时前
3.6 消防工程施工技术
笔记
handler017 小时前
算法:图的基本概念
c语言·开发语言·c++·笔记·算法·图论
之歆8 小时前
Day05_CSS完整博客笔记(上)
前端·css·笔记
YJlio8 小时前
《Windows Internals》10.5.1 ETW 概述:看懂 Windows 的“事件高速公路”
java·windows·笔记·stm32·嵌入式硬件·学习·eclipse
阿Y加油吧8 小时前
二刷 LeetCode:198. 打家劫舍 & 279. 完全平方数 复盘笔记
笔记·算法·leetcode
阿Y加油吧8 小时前
二刷 LeetCode:215. 数组中的第 K 个最大元素 & 347. 前 K 个高频元素 复盘笔记
笔记·leetcode·排序算法
pop_xiaoli8 小时前
【iOS】KVC与KVO
笔记·macos·ios·objective-c·cocoa