cocotb Makefile 流程:SIM/TOPLEVEL/MODULE 关键变量逐项拆解与回归
第一次跑通 cocotb 之后,每天打交道的其实不是 Python API,而是 Makefile 里那十几个大写变量。仿真器选谁、HDL 顶层是哪个模块、Python 测试文件叫什么名、参数从哪一侧透传——全靠它们接线。本文用一个参数化计数器做最小 DUT,在 cocotb 2.1.0 + Icarus Verilog 12.0 上真跑 11 组变量对照,把 cocotb Makefile 流程的每个旋钮讲透。最小 Python 测试平台的写法见 cocotb 最小测试平台 Python 模板,不依赖 cocotb 的通用仿真工程组织见 仿真工程 Makefile 通用模板。
本文要回答的问题
- cocotb 的 Makefile 里哪些变量必须有,各自喂给编译还是运行阶段?
TOPLEVEL和COCOTB_TEST_MODULES(旧名MODULE)都像「模块名」,到底指什么?- 只想跑一条用例、或一次回归多个 Python 测试文件,分别怎么写?
COMPILE_ARGS、EXTRA_ARGS、SIM_ARGS、plusarg 四条参数通道有什么区别?- 一次
make之后sim_build/里生成了什么?
一、Makefile 在 cocotb 流程里的位置
cocotb 自己不带仿真器,它是「Python 测试代码 + 仿真器 VPI 插件」的组合。Makefile 的职责就是把两边接起来:先调仿真器编译 HDL 生成仿真映像,再启动仿真器加载 cocotb 插件,由 Python 侧导入测试模块并跑用例。Makefile 变量按这条链路分成四类。
读这张图抓住一条分界线:上半组变量(蓝色、紫色)回答「编什么、用什么编」,下半组(红色、橙色)回答「跑谁的代码、额外参数走哪条通道」。最容易混的是紫色组的 TOPLEVEL 与红色组的 COCOTB_TEST_MODULES——前者是 Verilog 模块名,后者是 Python 文件名,二者只是恰好都被口头叫作「模块」。
二、关键变量逐项解读
本次实验的完整 Makefile 只有这些行,每个变量在后文的场景里都会被真实验证。
SIM ?= icarus
TOPLEVEL_LANG ?= verilog
VERILOG_SOURCES += $(CURDIR)/counter.v
TOPLEVEL := counter
# cocotb 2.1 名字;旧名 MODULE 仍是兼容别名
COCOTB_TEST_MODULES ?= test_counter
WIDTH ?=
ifneq ($(WIDTH),)
COMPILE_ARGS += -Pcounter.WIDTH=$(WIDTH)
endif
include $(shell cocotb-config --makefiles)/Makefile.sim| 变量 | 喂给谁 | 作用与不给的后果 |
|---|---|---|
SIM | 构建系统 | 选仿真器后端,如 icarus、verilator;不给则 cocotb 报错要求显式指定 |
TOPLEVEL_LANG | 构建系统 | HDL 语言,Verilog/SystemVerilog 写 verilog,VHDL 写 vhdl;与源码不符会报语言错误 |
VERILOG_SOURCES | iverilog 编译 | 源文件清单,多个文件空格分隔;为空则编译阶段没有设计 |
TOPLEVEL | iverilog -s | HDL 顶层模块名,仿真器把它作为根例化;拼错则「Unable to find the root module」 |
COCOTB_TEST_MODULES | Python 导入 | 测试模块名,不带 .py,多个用逗号分隔;拼错则 ModuleNotFoundError |
COCOTB_TEST_FILTER | 回归框架 | 正则筛用例名,只跑匹配项;不给则跑模块内全部 @cocotb.test() |
COMPILE_ARGS | 仅编译器 | 追加编译参数,如 -Pcounter.WIDTH=8 重绑参数、-D 传宏 |
EXTRA_ARGS | 编译与运行两侧 | 两边都透传,只适合两侧都认识的参数 |
SIM_ARGS | 仅仿真运行时 | iverilog 后端下追加给 vvp 的参数 |
COCOTB_PLUSARGS | DUT/Python | 传 +CYCLES=17 形式的 plusarg,Python 用 cocotb.plusargs 读取 |
三组新旧名字要记牢,cocotb 2.1 下旧名仍能用但会打 DeprecationWarning:Makefile 的 MODULE 改名 COCOTB_TEST_MODULES;TESTCASE 对应的内部变量是 COCOTB_TESTCASE,新名字为 COCOTB_TEST_FILTER;PLUSARGS 改名 COCOTB_PLUSARGS。top 语言不匹配的具体报错路径在 cocotb toplevel 语言错误排查,plusarg 的取值细节在 cocotb plusargs 报错页。
一个容易忽略的细节:COCOTB_TEST_FILTER 按正则子串匹配,test_reset 会同时命中 test_reset_value 和将来可能出现的 test_reset_twice;要精确选一条就写锚定形式 test_reset_value$。过滤后一条用例都不剩时 cocotb 只打警告、退出码仍是 0,CI 不能只看退出码。
TOPLEVEL 与 COCOTB_TEST_MODULES 的区别值得单独强调:TOPLEVEL=counter 让 iverilog 用 -s counter 找 Verilog 源码里的 module counter;COCOTB_TEST_MODULES=test_counter 让 Python 在运行时 import test_counter。两者没有任何名字上的联动——HDL 顶层叫 counter、测试文件叫 test_counter、甚至叫 tb_abc 都合法。一个面向编译产物,一个面向 Python 导入系统,这是高频坑的根源。
赋值符号也有讲究。SIM ?= icarus 表示命令行没给才用默认值,所以同一份 Makefile 可以 make SIM=icarus、make SIM=verilator 切换;而 TOPLEVEL := counter 写死是因为一个工程的 HDL 顶层不应被命令行随手覆盖。命令行变量在 GNU Make 里优先级最高,会整体替换文件内的同名赋值(包括 cocotb 内置规则里的追加),这一点在 COMPILE_ARGS 上会踩出真实故障,第四节展开。
三、变量如何变成两条命令:输入与输出
一次 make SIM=icarus 实际展开成「编译、运行」两条命令,变量沿不同边进入流程。
读这张图抓住两侧分离:编译侧吃 HDL 源文件与编译参数,运行侧吃仿真映像、运行参数与 Python 模块名。EXTRA_ARGS 之所以危险,正因为它会同时出现在两条边上。
输入文件有三类:HDL 设计(counter.v,一个 WIDTH 默认 4 的使能计数器)、Python 测试(test_counter.py 两条用例,另备一个 test_second.py 演示多模块)、Makefile 本身。DUT 全文只有十几行,参数化位宽是后面 -P 重绑实验的前提。
module counter #(
parameter WIDTH = 4
) (
input wire clk,
input wire rst_n,
input wire en,
output reg [WIDTH-1:0] q
);
always @(posedge clk) begin
if (!rst_n)
q <= {WIDTH{1'b0}};
else if (en)
q <= q + 1'b1;
end
endmodule测试侧读 plusarg 的一行是参数通道演示的落点:int(cocotb.plusargs.get("CYCLES", 3)) 取 +CYCLES=17,不给时默认 3;随后按这个数拉高 en 数拍,再比较 q。输出文件落在两处:sim_build/ 目录里有 sim.vvp(vvp 仿真映像)和 cmds.f(编译参数文件,内容是一行 +timescale+1ns/1ps,cocotb 靠它把时间精度固定为 1ns/1ps);工程根目录还有一份 results.xml(JUnit 格式的回归结果,CI 解析它而不是解析屏幕文本)。make clean 删除的正是这两类生成物,改编译参数后若不清理,旧映像可能被增量构建复用,这也是每个对照场景都先 make clean 的原因。
四、常见错与检查点
顶层名拼错,死在编译期。 TOPLEVEL=countr 时 iverilog 直接报 Unable to find the root module "countr",make 退出码 2。检查点:回看编译回显里的 -s <名字>,与 Verilog 源码的 module 声明逐字对。
测试模块名拼错,死在运行期。 COCOTB_TEST_MODULES=no_such_module 时编译完全正常,vvp 启动后 Python 侧抛 ModuleNotFoundError: No module named 'no_such_module'。检查点:文件名不带 .py、在工程根目录可被 python -c "import xxx" 导入。模块存在但收集到零条用例时退出码仍是 0,这个「零测试假绿」在 cocotb 最小测试平台 Python 模板里有完整演示。
命令行覆盖 COMPILE_ARGS 会冲掉 cocotb 的内部参数。 GNU Make 中命令行变量优先级最高,make COMPILE_ARGS="-Pcounter.WIDTH=8" 会把 cocotb 自己的 COMPILE_ARGS += -f sim_build/cmds.f 一起盖掉,编译回显里 -f sim_build/cmds.f 消失,时间精度退回 1 秒,所有 Clock(clk, 10, unit="ns") 立即抛 Unable to accurately represent 10(ns) with the simulator precision of 1e0。正确做法是在 Makefile 内 COMPILE_ARGS += ...(本文的 WIDTH 开关就是这么写的)。
EXTRA_ARGS 两侧透传,编译专用参数会毒到 vvp。 EXTRA_ARGS="-Wall" 让 iverilog 编译正常,却把 -Wall 原样追加给 vvp,报 vvp: invalid option -- 'W'。编译期参数走 COMPILE_ARGS,运行期参数走 SIM_ARGS(实测 SIM_ARGS="-n" 只出现在 vvp 命令行、不进编译行),只有两边都认识的参数才配用 EXTRA_ARGS。
五、真跑:11 组变量对照
基线运行与生成物如下,两条用例全部通过,汇总行 TESTS=2 PASS=2 FAIL=0,随后列出的 sim_build/ 里正是 cmds.f 与 sim.vvp 两件产物。
单用例筛选与两类「名字写错」的失败模式放在同一段证据里:COCOTB_TEST_FILTER=test_reset_value 把套件收窄成 (1/1);旧名 TESTCASE 仍生效但先打一条 COCOTB_TESTCASE is deprecated in favor of COCOTB_TEST_FILTER;再往下是 TOPLEVEL 拼错的编译期报错和测试模块拼错的 ModuleNotFoundError,退出码都是 2。
多模块回归用逗号列表 COCOTB_TEST_MODULES=test_counter,test_second,两个 Python 文件的 3 条用例被统一收集为 TESTS=3 PASS=3,第二个模块打的日志说明它与第一个模块共用同一个 HDL 顶层。
running test_counter.test_reset_value (1/3) ... passed
running test_counter.test_count_cycles (2/3) ... passed
21.00ns INFO cocotb.counter plusarg CYCLES=3
82.00ns INFO cocotb.counter after 3 enables q=3
running test_second.test_from_second_module (3/3) ... passed
** TESTS=3 PASS=3 FAIL=0 SKIP=0 **最后五组对照最能说明参数通道的差异。旧名 PLUSARGS="+CYCLES=17" 先打改名警告,4 位计数器计 17 拍回绕成 1,用例如期失败;Makefile 内 WIDTH=8 追加 -Pcounter.WIDTH=8 后配合新名 COCOTB_PLUSARGS,同样 17 拍得到 q=17 全过;命令行硬盖 COMPILE_ARGS 丢失 cmds.f 导致精度错误;EXTRA_ARGS="-Wall" 被 vvp 拒绝;SIM_ARGS="-n" 只出现在运行行且用例照过。
这五组证据给出的回归纪律是:想在 CI 里反复跑不同参数组合,把可配项做成 Makefile 内的条件追加(如 WIDTH),运行期变化走 plusarg,用 COCOTB_TEST_FILTER 选子集、逗号模块列表并全集,最后解析 results.xml 或 TESTS= 汇总行判定。
实验环境
- 机器/系统:Docker 容器 geek-eda:0.3-sta,Ubuntu 24.04
- 工具版本:cocotb 2.1.0(/opt/venvs/cocotb 虚拟环境)、Icarus Verilog 12.0 (stable)、Python 3.12.3、GNU Make 4.3
- 日期:2026-11-03
- 完整命令:
docker run --rm -v "$PWD":/work -w /work geek-eda:0.3-sta bash -lc 'source /opt/venvs/cocotb/bin/activate && bash run.sh';单组手工命令如make SIM=icarus COCOTB_TEST_FILTER=test_reset_value、make SIM=icarus WIDTH=8 COCOTB_PLUSARGS="+CYCLES=17"
代码获取
完整工程(counter.v、两个 Python 测试模块、Makefile、run.sh)位于公开仓 https://github.com/derekzhuo/geek-doc.cn/tree/main/experiments/flow-cocotb-makefile-flow。进入目录后执行上面的 docker 命令即可一次性复现全部 11 组对照;只跑基线用 make SIM=icarus。
这一篇在知识体系里的位置
本篇管「cocotb Makefile 变量怎么接线、怎么组织回归」。往上,第一版测试平台的时钟复位与断言写法看 cocotb 最小测试平台 Python 模板,不用 cocotb 的双后端仿真工程看 仿真工程 Makefile 通用模板;往排错方向走,语言不匹配看 cocotb toplevel 语言错误排查,plusarg 读不到看 cocotb plusargs 报错页,模块导入失败的完整堆栈看 cocotb NO_TEST_ERROR 排查。
总结
cocotb Makefile 流程的本质是给「编译、运行」两条命令分别喂变量:SIM 选后端,VERILOG_SOURCES 与 TOPLEVEL 指认 HDL 一侧,COCOTB_TEST_MODULES 与 COCOTB_TEST_FILTER 指认 Python 一侧,两组名字只是恰好都叫模块、互不相干;COMPILE_ARGS 只进编译且必须在 Makefile 内 +=,SIM_ARGS 只进运行,EXTRA_ARGS 两侧透传要慎用,COCOTB_PLUSARGS 把 + 参数送进 Python。11 组实测覆盖了单用例筛选、多模块回归、参数重绑与四类典型失败,生成物固定是 sim_build/ 的映像与 cmds.f 加根目录的 results.xml。