4. 仿真环境目录结构:项目根目录设计、RTL源码目录、验证组件目录、脚本目录、仿真结果目录
聊到仿真环境,我第一个想说的就是目录结构。
你可能觉得这玩意儿有啥好讲的?不就是建几个文件夹嘛。
嗯,我以前也这么想。直到有一次,一个项目做到一半,验证环境乱成一锅粥——脚本找不到RTL文件,仿真结果散落在各个角落,新来的同事看了三天都没搞明白东西在哪儿。那次之后,我花了整整一个周末重构了整个目录结构。说白了,一个好的目录结构,能让你的验证效率翻倍。
核心原则:目录结构要清晰、可扩展、易维护。让任何人看一眼就知道什么东西该放哪儿。
4.1 项目根目录设计
我个人习惯,项目根目录下只放最核心的东西。别把乱七八糟的文件都堆在根目录,那会让人崩溃。
一个典型的根目录长这样:
project_name/
├── rtl/ # RTL源码
├── verification/ # 验证组件
├── scripts/ # 脚本目录
├── sim_results/ # 仿真结果
├── docs/ # 文档
├── tools/ # 工具
├── Makefile # 顶层Makefile
└── README.md # 项目说明
你看,一共就7个东西。每个目录做什么,名字已经说清楚了。
这里有个小技巧:根目录下的Makefile一定要写。我见过太多项目,脚本散落在各个目录里,每次跑仿真都要手动敲一长串命令。有了顶层Makefile,你只需要 make sim 或者 make clean,省心多了。
我的经验:Makefile里至少提供这几个目标:sim(跑仿真)、clean(清理结果)、regression(回归测试)、wave(打开波形)。这样团队里所有人都能用同一套命令。
4.2 RTL源码目录
RTL目录的设计,说白了就是怎么组织你的设计代码。
我建议按模块来分。别把所有.v文件都扔到一个目录里,那跟把衣服全塞一个衣柜没啥区别——找的时候得翻半天。
rtl/
├── top/ # 顶层模块
│ └── chip_top.v
├── core/ # 核心模块
│ ├── core_alu.v
│ ├── core_ctrl.v
│ └── core_regfile.v
├── peripheral/ # 外设模块
│ ├── uart.v
│ ├── spi.v
│ └── gpio.v
├── memory/ # 存储模块
│ ├── sram_wrapper.v
│ └── rom_wrapper.v
└── include/ # 头文件
├── defines.vh
└── parameters.vh
为什么会这样分?你想想看,当你要修改UART模块时,你只需要去 rtl/peripheral/ 目录下找。如果所有文件都混在一起,你得先搞清楚哪个文件是UART,然后再去翻。
注意:头文件目录 include/ 一定要单独放。我曾经吃过一次亏,把 `defines.vh` 放在某个模块目录下,结果其他模块引用时路径写错了,编译报错查了半天。后来我统一把所有头文件放到 rtl/include/,再也没出过这种问题。
4.3 验证组件目录
验证组件这块,我习惯按验证方法学的思路来组织。说白了,就是UVM那套东西——但目录结构要清晰,别搞得太复杂。
verification/
├── tb/ # 测试平台
│ ├── tb_top.sv
│ └── tb_config.sv
├── agents/ # 代理
│ ├── uart_agent/
│ ├── spi_agent/
│ └── gpio_agent/
├── sequences/ # 序列
│ ├── base_seq.sv
│ └── test_seq.sv
├── tests/ # 测试用例
│ ├── test_base.sv
│ ├── test_smoke.sv
│ └── test_regression.sv
├── models/ # 参考模型
│ └── ref_model.sv
├── coverage/ # 覆盖率
│ └── cov_collector.sv
└── env/ # 验证环境
└── env_top.sv
每个agent目录下,我习惯再分一层:
uart_agent/
├── uart_driver.sv
├── uart_monitor.sv
├── uart_sequencer.sv
└── uart_agent_pkg.sv
这样设计的好处是:每个agent都是独立的,可以复用。我在一个项目里写好的UART agent,拿到另一个项目里,改一下配置就能直接用。
避坑指南:我曾经把所有的sequence都写在一个文件里,结果文件越来越大,最后到了3000多行。找某个sequence得翻半天。后来我改成每个test一个文件,清爽多了。
4.4 脚本目录
脚本目录,嗯,这里我要多说两句。很多人不重视脚本的组织,觉得能跑就行。但你想过没有——当项目有几十个脚本时,你还能找到哪个是干嘛的吗?
scripts/
├── sim/ # 仿真脚本
│ ├── run_sim.sh
│ ├── compile.sh
│ └── wave.sh
├── regression/ # 回归脚本
│ ├── run_regression.sh
│ └── report_gen.sh
├── analysis/ # 分析脚本
│ ├── coverage_merge.sh
│ └── log_parser.py
└── utils/ # 工具脚本
├── clean.sh
└── backup.sh
我个人习惯,脚本用 .sh 或者 .py。别用 .bat,除非你确定团队里所有人都用Windows。说实话,在IC验证这个领域,Linux才是主流。
每个脚本的开头,我建议加一段注释:
#!/bin/bash
#=============================================
# 脚本功能:运行单个测试用例
# 用法:./run_sim.sh <test_name> [seed]
# 作者:xxx
# 最后修改:2024-01-15
#=============================================
别小看这几行注释。我遇到过好几次,脚本是半年前写的,自己都忘了怎么用。有了注释,看一眼就知道。
注意:脚本的路径引用一定要用相对路径,或者通过环境变量。我见过有人写死路径,结果项目换个目录就全废了。用 $(dirname "$0") 获取脚本所在目录,然后基于它去引用其他文件,这样最靠谱。
4.5 仿真结果目录
仿真结果目录,说白了就是你的仿真输出放哪儿。这个目录设计得好不好,直接影响到你调试的效率。
sim_results/
├── test_smoke/ # 单个测试用例结果
│ ├── seed_1/
│ ├── seed_2/
│ └── seed_3/
├── test_regression/
│ ├── seed_1/
│ └── seed_2/
├── coverage/ # 覆盖率结果
│ ├── merged/
│ └── reports/
├── logs/ # 日志
│ └── sim.log
└── waves/ # 波形文件
└── test_smoke.fsdb
每个测试用例的结果目录里,我习惯放这些文件:
- sim.log:仿真日志,记录所有打印信息
- wave.fsdb/vcd:波形文件,用于调试
- coverage_report:覆盖率报告
- test_status:测试结果(PASS/FAIL)
为什么要按seed分目录?你想想看,同一个测试用例跑不同的随机种子,结果可能不一样。如果不分开,后一次的结果会把前一次的覆盖掉,到时候想对比都对比不了。
我的习惯:每次跑完仿真,自动生成一个 summary.html 文件,里面列出所有测试用例的结果、运行时间、覆盖率等信息。这样一眼就能看出哪些用例过了,哪些挂了。省得一个个去翻日志。
4.6 整体结构图
说了这么多,我画个图帮你理一理思路。这个图展示了整个仿真环境的目录结构,以及各个目录之间的关系。
4.7 总结一下
目录结构这事儿,说白了就是「规矩」。规矩立好了,团队里所有人都按同一套来,效率自然就上去了。
我最后再啰嗦一句:目录结构不是一成不变的。项目初期可以简单点,随着项目变大再慢慢细化。别一开始就搞得太复杂,那反而会让人望而却步。
嗯,关于目录结构就聊到这儿。记住我说的那几个原则,你的仿真环境搭建就能少走很多弯路。