集成电路设计
我的数字集成电路设计工具链
从 Linux 环境、版本管理、仿真验证到综合时序检查,整理一个可复用的数字 IC 学习与研究工具链。
数字集成电路设计不是单个工具的堆叠,而是一条需要持续校准的工程流程。第一版工具链笔记只记录公开、通用的经验,不包含任何受限 PDK、授权脚本或未确认项目数据。
我更倾向于把工具链理解成一套“可复现的证据系统”:每一个设计判断都应该能追溯到脚本、配置、输入向量、仿真日志或报告。这样做的好处不是让目录看起来更整洁,而是在设计规模变大、版本迭代变快之后,仍然可以回答三个基本问题:
- 这个结果是由哪一版 RTL 和哪一组约束生成的?
- 这个 bug 是模型、RTL、testbench 还是脚本引入的?
- 当前修改有没有破坏已经通过的基本功能?
下面整理的是我在学习和科研原型中使用的一套通用组织方式。它不依赖某个特定商业环境,也不涉及工艺库、授权服务器或不可公开的项目配置。
工具链分层
| 层级 | 常用工具 | 关注点 |
|---|---|---|
| 建模 | Python, NumPy, PyTorch | 算法正确性、定点化边界 |
| RTL | Verilog, SystemVerilog | 时序友好、接口清晰 |
| 仿真 | VCS, Verdi | 可重复回归、波形定位 |
| 质量 | Verilator, SpyGlass | Lint、CDC、编码规范 |
| 实现 | Design Compiler, PrimeTime | 约束、面积、时序、功耗 |
这几层之间不应该是松散的。比较理想的状态是:Python 模型能生成 RTL 仿真需要的测试向量;RTL 仿真能输出可被 Python 脚本检查的结果;综合和时序报告能被脚本收集成统一格式;文档中记录的是流程入口,而不是手工操作截图。
环境基线
一个可维护的数字 IC 环境至少应该固定以下信息:
| 项目 | 建议记录方式 | 原因 |
|---|---|---|
| 操作系统 | uname -a、发行版版本 | 不同系统的 shell、库和路径行为可能不同 |
| EDA 版本 | 工具启动日志或 -version 输出 | 不同版本的 warning、语法支持和报告格式可能变化 |
| 仿真选项 | Makefile 或脚本参数 | 保证编译和运行参数可复现 |
| 随机种子 | 日志文件、case 配置 | 便于复现随机验证中的失败 |
| Git 提交 | 报告目录中的 commit hash | 将结果绑定到具体源码状态 |
我通常会在 scripts/env/ 或项目根目录下保留一个很小的环境检查脚本,只检查必要条件,不把复杂流程写死在里面。
#!/usr/bin/env bash
set -euo pipefail
echo "[env] host: $(hostname)"
echo "[env] kernel: $(uname -r)"
echo "[env] git: $(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
command -v vcs >/dev/null 2>&1 && vcs -ID || echo "[warn] vcs not found"
command -v verdi >/dev/null 2>&1 && verdi -version | head -n 1 || echo "[warn] verdi not found"
这类脚本不应该替代模块化的仿真、综合或检查脚本。它的作用只是让人在进入项目后快速知道“当前机器是否具备基本条件”。
目录约定
project/
model/
rtl/
include/
tb/
sim/
scripts/
constraints/
docs/
reports/
目录的目标是让 RTL、测试平台、脚本和报告互不混杂。每个工具产生的中间文件应放在可清理目录中,避免污染源代码。一个更具体的划分可以是:
| 目录 | 内容 | 是否应进入版本管理 |
|---|---|---|
model/ | Python/C++ 参考模型、定点化脚本 | 是 |
rtl/ | 可综合 RTL | 是 |
include/ | 参数、宏、package | 是 |
tb/ | testbench、driver、monitor、scoreboard | 是 |
scripts/ | 仿真、检查、报告收集脚本 | 是 |
constraints/ | 学习用约束模板或公开约束 | 视情况 |
sim/ | 仿真工作目录 | 否 |
reports/ | 自动生成报告 | 通常否,关键报告可归档 |
对初学项目来说,目录不必一开始就非常复杂。但当源文件超过十几个、测试用例超过几组之后,清晰目录会直接影响调试效率。
版本管理策略
数字 IC 项目中的 Git 管理容易出现两个问题:一是把大量工具中间文件提交进去,二是脚本和报告无法对应。我的基本习惯是:
- 源码、脚本、约束模板和文档进入版本管理。
- 仿真波形、编译产物、缓存目录和大报告默认忽略。
- 对重要实验结果,单独写一份短 Markdown 记录命令、commit、参数和结论。
- 每次结构性修改尽量让 commit 主题对应一个明确目标,例如
rtl: add systolic array control fsm。
一个项目的 .gitignore 可以从非常小的版本开始:
# simulation
simv
csrc/
*.vpd
*.vcd
*.fsdb
*.log
*.key
# reports and tool outputs
reports/
work/
*.rpt
*.syn
# editor
.vscode/
*.swp
如果需要保留某些报告,建议放在 docs/experiments/ 中,并在文件名中包含日期和简短主题,而不是直接提交工具生成的整个目录。
Makefile 入口
SIM_TOP ?= tb_top
RTL := $(shell find rtl -name "*.v")
TB := tb/$(SIM_TOP).sv
SEED ?= 1
.PHONY: sim clean lint regress wave
sim:
vcs -full64 -sverilog $(RTL) $(TB) -o simv
./simv +ntb_random_seed=$(SEED)
lint:
scripts/run_lint.sh
regress:
scripts/run_regress.sh --seed $(SEED)
wave:
verdi -ssf wave.fsdb &
clean:
rm -rf simv csrc *.log *.vpd
Makefile 的价值是提供稳定入口,而不是把所有逻辑堆到一个文件里。随着流程变复杂,可以把具体命令拆到 scripts/ 目录中,Makefile 只负责暴露常用目标。
我一般会避免在 README 中只写一串很长的命令。长命令一旦需要复制粘贴,就说明它应该被整理成脚本。
仿真验证入口
最小仿真流程通常包括三类测试:
| 测试类型 | 目的 | 示例 |
|---|---|---|
| Smoke test | 确认模块能启动并完成基本事务 | 复位、单次 start/done |
| Directed test | 覆盖明确边界条件 | 全零输入、最大值、尺寸边界 |
| Random test | 扩大输入空间 | 随机张量、随机 stall、随机 seed |
即使不用完整 UVM,也建议建立 driver、monitor 和 scoreboard 的基本分工:
testcase
├─ driver: 把输入事务送入 DUT
├─ monitor: 采集 DUT 输出事务
└─ scoreboard:与参考模型结果对比
这能避免 testbench 逐渐变成无法维护的单文件脚本。对于算法类模块,scoreboard 最好不要只检查“有输出”,而要检查数值、顺序、边界和异常情况。
波形与日志
波形文件适合定位问题,但不适合作为第一层验证证据。日常流程中,我更希望先看到结构化日志:
[case] conv_3x3_stride1_seed_17
[cfg ] input=8x16x16 weight=16x8x3x3 dtype=int8 acc=int32
[pass] max_abs_error=0 mismatches=0 cycles=...
这里的 cycles 只是内部调试信息。如果没有固定频率、约束、平台和测量方法,就不应该把它写成对外性能结论。日志的主要价值是帮助定位回归失败。
Lint 和编码规范
Lint 不是最后才做的“质量装饰”,而应该尽早进入循环。常见检查包括:
- 未使用信号、隐式线网和位宽截断。
- 不完整赋值导致的 latch 推断。
- 组合逻辑环路。
- 复位风格不一致。
- 阻塞和非阻塞赋值混用。
- 时钟域交叉和异步复位释放风险。
在学习项目中,不必一开始追求零 warning,但应当对每个保留 warning 有解释。最危险的是 warning 数量太多,导致真正的问题被淹没。
综合与时序检查
综合前至少要有三类输入:
- 可综合 RTL 文件列表。
- 约束文件,例如时钟、输入输出延迟、时钟不确定性。
- 面向当前阶段的报告目标,例如面积、关键路径、未约束路径。
一个非常抽象的约束模板可以写成:
create_clock -name clk -period 5.000 [get_ports clk]
set_clock_uncertainty 0.100 [get_clocks clk]
set_input_delay 0.500 -clock clk [remove_from_collection [all_inputs] [get_ports clk]]
set_output_delay 0.500 -clock clk [all_outputs]
具体项目中不能机械套用模板。时钟周期、I/O 约束、false path、multicycle path 都必须来自真实架构假设和接口时序。这里给出的只是“综合流程需要约束入口”的示意。
定点化检查
硬件实现前需要明确数值范围。若输入为 ,缩放因子为 ,常见量化形式可以写成:
这条公式只是基本表达,实际项目还要根据溢出策略、舍入方式和累加位宽做验证。
定点化验证中,我会特别关注三类输入:
- 正常分布输入,用于观察平均误差。
- 极值输入,用于检查饱和和溢出策略。
- 人工构造输入,用于触发边界条件,例如卷积窗口刚好跨越 padding 区域。
如果算法模型和 RTL 使用不同舍入策略,即使功能结构正确,也可能出现稳定的一位误差。因此在写 RTL 前,最好把舍入、截断、饱和和符号扩展规则写成明确文档。
def saturate(value: int, width: int) -> int:
low = -(1 << (width - 1))
high = (1 << (width - 1)) - 1
return max(low, min(high, value))
这种小函数看似简单,但它可以作为 Python 参考模型、测试向量生成和 RTL 对拍之间的共同语义。
流程建议
- 先写可运行的软件参考模型。
- 固定输入输出张量、位宽和误差阈值。
- 编写 RTL 并建立最小 testbench。
- 加入随机测试、边界测试和回归脚本。
- 在综合前做 lint、约束和面积预估。
一次修改的推荐闭环
当我修改一个 RTL 模块时,尽量让流程控制在一个可重复闭环中:
edit RTL
↓
run lint
↓
run smoke simulation
↓
run directed cases
↓
run small random regression
↓
collect report and commit
如果某一步失败,不要急着继续堆修改。先把失败样例缩小到最小可复现输入,再决定是修 RTL、修模型还是修 testbench。对硬件设计来说,定位路径本身就是设计质量的一部分。
常见问题
是否一开始就需要 UVM?
不一定。对于单个数据通路模块,轻量 SystemVerilog testbench 加 Python 参考模型通常更高效。等接口协议、事务类型和覆盖率目标变复杂后,再引入 UVM 会更自然。
Verilog 还是 SystemVerilog?
如果工具环境允许,SystemVerilog 的 logic、package、interface、always_ff 和 always_comb 能显著提升可读性。但面向可综合 RTL 时,仍然要确认目标工具链支持的语法子集。
是否需要 Docker?
开源工具链可以考虑 Docker 固化环境。商业 EDA 工具通常涉及授权和系统依赖,是否容器化要看实验室或服务器环境,不建议为了形式统一而引入额外复杂度。
阶段性总结
一套好的数字 IC 工具链不应该只追求“能跑通一次”,而应该能支撑反复修改、定位和回归。对个人学习和科研原型来说,最重要的是把模型、RTL、验证、脚本和文档连接起来,逐步形成可复现的工程闭环。
后续我会继续补充 CDC、UPF、约束模板、报告解析和脚本工程化方式。所有具体报告数据都应来自可公开项目或个人可披露实验。