cocotb plusargs 传参失败:make 到 Python 的两条参数通道
cocotb plusargs 传参失败时,常见现象不是仿真器不能启动,而是 cocotb.plusargs 为空、值为 None,或 Python 拿到了意料之外的类型。本文用 cocotb 2.1.0 与 Icarus Verilog 跑通最小 Makefile,并对照未导出环境变量、漏传 plusargs、错用 SIM_ARGS 三种情况。如果日志根本没有进入测试函数,还应检查 cocotb 测试模块导入与 no tests 排查,不要把发现阶段错误当成取值错误。
本文要回答的问题
COCOTB_PLUSARGS与操作系统环境变量分别经过哪条链路进入 Python?- cocotb 2.1.0 的最小 Makefile 和正确命令怎样写?
+REPEAT=3与纯开关+VERBOSE读出来各是什么类型?cocotb.plusargs={}、RUN_ID=None和Unable to open input file分别该改哪里?
一、先分清两条通道,cocotb plusargs 传参失败才有可查的边界
plusargs 是传给仿真器进程的 +名称=值 参数,cocotb 把它整理成类似字典的 cocotb.plusargs;环境变量则属于进程环境,用 os.environ 读取。二者可以在同一条 make 命令里设置,但不会互相转换。
蓝色是参数入口与 make 展开,橙色是仿真器命令行,紫色是环境继承,绿色才是 Python 读取点;红色分支会在测试执行前失败。排查时先确定自己要看哪条通道:改 COCOTB_PLUSARGS 不会自动生成 os.environ 项,给 shell 设置 MODE 也不会自动生成 +MODE。
二、cocotb 2.1.0 最小 Makefile 与正确命令
实验 RTL 只有一个延迟结束仿真的 config_probe 模块,因此日志不会被业务逻辑干扰。Makefile 只需指定源码、顶层和测试模块,再包含 cocotb 提供的仿真规则:
TOPLEVEL_LANG = verilog
VERILOG_SOURCES = $(PWD)/config_probe.v
TOPLEVEL = config_probe
COCOTB_TEST_MODULES = test_plusargs
include $(shell cocotb-config --makefiles)/Makefile.sim新版使用 COCOTB_TEST_MODULES 指定 Python 模块,运行参数使用 COCOTB_PLUSARGS。下面这条命令同时走两条通道:
RUN_ID=env_42 make COCOTB_PLUSARGS="+MODE=fast +REPEAT=3 +VERBOSE"双引号让 shell 把 COCOTB_PLUSARGS=... 保留为一个命令参数;进入 make 配方后,变量内容再展开为三个独立的 vvp 参数。这里发生两次处理是正常现象,不代表引号会原样进入 cocotb.plusargs。
这里还要区分运行参数与 HDL 编译参数。COCOTB_PLUSARGS 只改变本次仿真的输入,不会重新参数化 RTL,也不会替代时钟、复位等必要激励。如果配置必须影响位宽或生成块,应使用对应仿真器的编译期参数入口,并重新编译;如果配置只是测试次数、日志开关或激励模式,plusargs 更合适。把两类配置混在一条命令里时,要分别确认它们出现在编译命令和 vvp 命令的什么位置。
RUN_ID 这类仅用于标记回归分组、选择输出名称的值则不必经过仿真器。让它留在环境变量里,可以避免 HDL 侧的 $value$plusargs 与 Python 侧同时解析一份并不关心的配置。
| 写法 | 实际作用 | Python 读取位置 |
|---|---|---|
COCOTB_PLUSARGS="+MODE=fast" | 给仿真器传 +MODE=fast | cocotb.plusargs["MODE"] |
RUN_ID=env_42 make ... | 仅给这次 make 及其子进程导出变量 | os.environ["RUN_ID"] |
RUN_ID=shell_only; make ... | 只改当前 shell 变量,未导出 | os.environ.get("RUN_ID") 为 None |
不要把第三行与 make RUN_ID=value 混为一谈:后者是 GNU make 的命令行变量,满足命名条件时会按 make 的规则导出。本文错误组特意采用未 export 的 shell 变量,避免把两种机制写成同一个结论。
三、真实取值:带等号的是字符串,纯开关是布尔值
测试代码直接打印两个字典入口,再对值和类型做断言。关键部分如下:
import os
import cocotb
plusargs = dict(cocotb.plusargs)
run_id = os.environ.get("RUN_ID")
assert plusargs["MODE"] == "fast"
assert plusargs["REPEAT"] == "3"
assert plusargs["VERBOSE"] is True
assert run_id == "env_42"结果中 MODE 是字符串 fast,REPEAT 也是字符串 3,不能默认它已经成为整数;不带等号的 VERBOSE 则是布尔值 True,不是空字符串。同一组日志还显示 RUN_ID='env_42'、PASS=1 FAIL=0 和 GOOD_EXIT=0,说明两个读取入口都经过了真实仿真。
需要数值时应显式转换并检查范围,需要开关时直接做布尔判断:
repeat = int(cocotb.plusargs.get("REPEAT", "1"))
if repeat <= 0:
raise ValueError("REPEAT must be positive")
verbose = cocotb.plusargs.get("VERBOSE", False)显式转换还有一个好处:int("abc") 的异常会指向参数内容,而不是等到循环或比较运算时才出现难懂的类型错误。
四、三种错误传参对应三种不同证据
对照实验没有靠猜错误信息,而是分别改变一个条件。结果汇总如下:
| 实验改动 | 关键输出 | 根因与修复 |
|---|---|---|
仅执行 RUN_ID=shell_only,不导出 | plusargs 完整,但 RUN_ID=None | 改用命令前临时赋值,或执行 export RUN_ID |
完全省略 COCOTB_PLUSARGS | cocotb.plusargs={},环境变量仍正确 | 把 +MODE=... 放回该变量 |
把 plusargs 放进 SIM_ARGS | +MODE=fast: Unable to open input file | Icarus 参数位置错误,改回 COCOTB_PLUSARGS |
未导出环境变量时,仿真与 plusargs 解析都正常,失败点只在环境断言:
1.00ns INFO test cocotb.plusargs={'MODE': 'fast', 'REPEAT': '3', 'VERBOSE': True}
1.00ns INFO test os.environ RUN_ID=None
AssertionError: RUN_ID must be exported through the process environment
BAD_ENV_EXIT=2错用 SIM_ARGS 则更早:vvp 命令中 +MODE=fast 位于设计文件之前,Icarus 把它当成输入文件,Python 测试尚未运行。
+MODE=fast: Unable to open input file.
BAD_SIM_ARGS_EXIT=2所以看到 cocotb.plusargs={} 时检查是否漏传,看到 None 时检查环境导出,看到 Unable to open input file 时先检查 vvp 命令中的参数顺序。三种故障不应都用“重装 cocotb”处理。
定位时可以沿三层向下缩小范围。先看 make 打印出的最终命令,确认加号参数是否到达 vvp、位于设计文件之前还是之后;再看 Python 入口打印的完整字典和环境值,确认问题发生在传递还是读取;最后才检查断言与类型转换。若最终命令里已经没有参数,改 Python 没有意义;若字典里已有正确值,继续调整 shell 引号也只是在绕路。
失败组的退出码也有用途。本文正确组为 0,三个错误组均为 2,但数字相同并不代表根因相同:BAD_ENV_EXIT=2 前能看到 Python 断言,BAD_SIM_ARGS_EXIT=2 前只有 vvp 的文件错误。退出码适合告诉 CI 回归失败,具体修复仍要依据它前面的第一条有效错误。
五、把参数检查集中在测试入口
真实项目通常有多个测试协程。如果每个协程各自读取、转换并给默认值,同一参数可能在一个用例里是整数、在另一个用例里还是字符串,缺失参数也会到不同时间才暴露。更稳妥的做法是在公共入口解析一次,把校验后的配置传给各用例。
def read_config():
mode = cocotb.plusargs.get("MODE", "basic")
try:
repeat = int(cocotb.plusargs.get("REPEAT", "1"))
except ValueError as exc:
raise ValueError("REPEAT must be an integer") from exc
if repeat <= 0:
raise ValueError("REPEAT must be positive")
run_id = os.environ.get("RUN_ID")
if run_id is None:
raise ValueError("RUN_ID is not exported")
return mode, repeat, run_id这段代码把可选参数与必需参数分开:MODE 和 REPEAT 有明确默认值,RUN_ID 缺失则立即报错。VERBOSE 一类纯开关可以直接使用布尔判断,不需要再写 == "True"。错误消息应指出参数名和要求,不要只打印“配置错误”。
入口校验完成后,再打印一次规范化结果,例如模式、次数和运行编号。这样同一份日志既能回答“命令传了什么”,也能回答“测试实际采用了什么”。对于默认值尤其重要:没有传 REPEAT 与显式传入 REPEAT=1 可以得到相同行为,但日志里记录最终值,复现时就不必翻查代码。
也不要用宽泛的 except Exception 吞掉转换错误后静默退回默认值。那会把拼写错误变成另一套测试行为,让回归表面通过、实际覆盖却发生变化。默认值只服务“参数不存在”,格式错误和越界应当让测试尽早失败。
集中解析还便于复用:新增用例只接收已经校验的配置对象,不再重复依赖全局字典。以后更换参数入口时,修改范围也能限制在这一处。
代码获取:完整 RTL、Python 测试、最小 Makefile 与四组一键实验位于公开仓
experiments/ts-cocotb-plusargs/。准备好文中 Docker 环境后执行bash run.sh,脚本要求正确组通过、三组错误用例按预期失败。
实验环境
- 机器/系统:Linux x86_64 Docker 容器 / Ubuntu 24.04.5 LTS
- 工具版本:cocotb 2.1.0、Icarus Verilog 12.0 (stable)、cocotb 虚拟环境 Python 3.12.12
- 日期:2026-10-31
- 完整命令:
bash run.sh这一篇在知识体系里的位置
本篇关注参数已经写到命令后,怎样穿过 make、仿真器和进程环境到达 Python。cocotb no tests to run 排查处理测试模块发现失败;cocotb BinaryValue 迁移与 LogicArray 转换处理值对象 API。三者分别看命令行、测试发现和值转换,不应混用修复手段。
总结
cocotb plusargs 传参失败要先分通道:COCOTB_PLUSARGS 对应 cocotb.plusargs,已导出的环境变量对应 os.environ。cocotb 2.1.0 实测 +REPEAT=3 读出字符串、+VERBOSE 读出 True;漏传得到空字典,未导出环境变量得到 None,错放 SIM_ARGS 则会让 Icarus 在测试前报错。用真实打印值、类型和退出码定位,比反复改引号更可靠。