集成电路设计

我的数字集成电路设计工具链

从 Linux 环境、版本管理、仿真验证到综合时序检查,整理一个可复用的数字 IC 学习与研究工具链。

目录工具链分层环境基线目录约定版本管理策略Makefile 入口仿真验证入口波形与日志Lint 和编码规范综合与时序检查定点化检查流程建议一次修改的推荐闭环常见问题是否一开始就需要 UVM?Verilog 还是 SystemVerilog?是否需要 Docker?阶段性总结

数字集成电路设计不是单个工具的堆叠,而是一条需要持续校准的工程流程。第一版工具链笔记只记录公开、通用的经验,不包含任何受限 PDK、授权脚本或未确认项目数据。

我更倾向于把工具链理解成一套“可复现的证据系统”:每一个设计判断都应该能追溯到脚本、配置、输入向量、仿真日志或报告。这样做的好处不是让目录看起来更整洁,而是在设计规模变大、版本迭代变快之后,仍然可以回答三个基本问题:

  • 这个结果是由哪一版 RTL 和哪一组约束生成的?
  • 这个 bug 是模型、RTL、testbench 还是脚本引入的?
  • 当前修改有没有破坏已经通过的基本功能?

下面整理的是我在学习和科研原型中使用的一套通用组织方式。它不依赖某个特定商业环境,也不涉及工艺库、授权服务器或不可公开的项目配置。

工具链分层

层级常用工具关注点
建模Python, NumPy, PyTorch算法正确性、定点化边界
RTLVerilog, SystemVerilog时序友好、接口清晰
仿真VCS, Verdi可重复回归、波形定位
质量Verilator, SpyGlassLint、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 管理容易出现两个问题:一是把大量工具中间文件提交进去,二是脚本和报告无法对应。我的基本习惯是:

  1. 源码、脚本、约束模板和文档进入版本管理。
  2. 仿真波形、编译产物、缓存目录和大报告默认忽略。
  3. 对重要实验结果,单独写一份短 Markdown 记录命令、commit、参数和结论。
  4. 每次结构性修改尽量让 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 数量太多,导致真正的问题被淹没。

综合与时序检查

综合前至少要有三类输入:

  1. 可综合 RTL 文件列表。
  2. 约束文件,例如时钟、输入输出延迟、时钟不确定性。
  3. 面向当前阶段的报告目标,例如面积、关键路径、未约束路径。

一个非常抽象的约束模板可以写成:

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 都必须来自真实架构假设和接口时序。这里给出的只是“综合流程需要约束入口”的示意。

定点化检查

硬件实现前需要明确数值范围。若输入为 xx,缩放因子为 SS,常见量化形式可以写成:

q=clip(round(xS),qmin,qmax)q = \mathrm{clip}(\mathrm{round}(x \cdot S), q_{min}, q_{max})

这条公式只是基本表达,实际项目还要根据溢出策略、舍入方式和累加位宽做验证。

定点化验证中,我会特别关注三类输入:

  • 正常分布输入,用于观察平均误差。
  • 极值输入,用于检查饱和和溢出策略。
  • 人工构造输入,用于触发边界条件,例如卷积窗口刚好跨越 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 对拍之间的共同语义。

流程建议

本笔记中的工具链是学习和科研原型视角,不代表某个具体商业项目的签核流程。
  1. 先写可运行的软件参考模型。
  2. 固定输入输出张量、位宽和误差阈值。
  3. 编写 RTL 并建立最小 testbench。
  4. 加入随机测试、边界测试和回归脚本。
  5. 在综合前做 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 的 logicpackageinterfacealways_ffalways_comb 能显著提升可读性。但面向可综合 RTL 时,仍然要确认目标工具链支持的语法子集。

是否需要 Docker?

开源工具链可以考虑 Docker 固化环境。商业 EDA 工具通常涉及授权和系统依赖,是否容器化要看实验室或服务器环境,不建议为了形式统一而引入额外复杂度。

阶段性总结

一套好的数字 IC 工具链不应该只追求“能跑通一次”,而应该能支撑反复修改、定位和回归。对个人学习和科研原型来说,最重要的是把模型、RTL、验证、脚本和文档连接起来,逐步形成可复现的工程闭环。

后续我会继续补充 CDC、UPF、约束模板、报告解析和脚本工程化方式。所有具体报告数据都应来自可公开项目或个人可披露实验。

从算法模型到可综合 RTL 的完整流程

记录神经网络算子从 Python 模型、定点化、接口定义到可综合 RTL 的工程拆解方法。

VCS 常用命令与仿真流程

整理 VCS 仿真的常用命令、编译运行分离、波形生成和回归脚本组织方式。

RISC-V 矩阵处理器的基本架构

从 ISA 扩展、寄存器组织、矩阵乘数据通路和软件接口角度整理 RISC-V 矩阵处理器原型思路。