cocotb BinaryValue 用法报错:迁移 LogicArray 与 binstr 转换边界
照着旧教程写 BinaryValue(..., binaryString=...),升级后却在测试开始前直接失败,是典型的 cocotb BinaryValue 用法报错。这里真正需要区分的是三件事:旧类是否还能导入、字符串现在怎样读写,以及含 X/Z 的四态值为何不能转成整数。本篇使用 cocotb 2.1.0 与 Icarus Verilog 实跑旧、新两组 testbench;如果失败信息是找不到整个测试模块,则改走 cocotb 测试模块导入排查,不要与本次 API 迁移混为一谈。
本文要回答的问题
- cocotb 2.1.0 中
from cocotb.binary import BinaryValue的真实错误是什么? binaryString写初值和binstr读字符串分别怎样迁移?dut.signal.value读出来是什么类型,什么时候可以用int()?- 怎样用最小回归确认修复不是只在静态代码里“看起来正确”?
一、cocotb BinaryValue 用法报错的真实现场
旧 testbench 的关键代码只有两行,意图是把 8 位信号写成 00001010,再读取二进制字符串:
from cocotb.binary import BinaryValue
dut.data.value = BinaryValue(0, n_bits=8, binaryString="00001010")
print(dut.data.value.binstr)终端卡里最有价值的是最后三行,而不是前面一长串导入栈:执行到 legacy_test.py 第 2 行便抛出 ModuleNotFoundError: No module named 'cocotb.binary'。仿真器已经启动,日志也明确显示 cocotb 2.1.0,但测试函数尚未运行,所以把 binaryString 改成别的关键字并不能救活这份代码——解释器根本没走到构造对象那一步。
这也意味着排查顺序应当是先删旧导入,再处理赋值与读取。不要根据 ModuleNotFoundError 去安装一个名为 cocotb.binary 的第三方包;它不是缺失的依赖,而是当前版本里已经不复存在的内部模块路径。
为什么 RTL 编译成功仍找不到测试?cocotb 启动后要先导入 Python 测试模块,才能扫描带 @cocotb.test() 的协程。旧导入位于文件顶层,因此异常发生在“发现测试”阶段,而不是某个断言执行阶段。判断时可以看日志有没有 running ... (1/N):本文旧用例没有这一行,新版才有。若错误栈定位到你的 .py 文件和 cocotb.binary,改 API 即可;若栈里根本没有目标文件,才需要检查测试模块配置与搜索路径。
这里还要避免一个误导性动作:单独在交互式解释器里试 LogicArray(..., binaryString=...),然后把新参数报错当成原始故障。原始旧代码的第一处失败是模块导入,后续参数问题并没有获得执行机会。排错永远先修 traceback 指向的第一处,再运行到下一处,不要一次性猜完所有可能错误。
二、迁移关系:对象、字符串与整数分开替换
新版写法不是把类名机械替换后仍保留所有旧参数,而是按用途拆开:已知二进制文本用 LogicArray 构造,DUT 信号仍经 .value 读写,显示文本用 str(),确定只有 0/1 时才用 int()。
红色节点是本次失败点,蓝色是 DUT 访问入口,橙色表示需要主动选择的转换,绿色是文本读取出口。读图时不要把 LogicArray 理解成单纯的字符串包装:它保留位宽与四态信息,所以 00001010 转字符串仍有 8 位,转整数才会得到 10;前导零只在文本表示中保留。
常用写法可以按下面这张表迁移:
| 旧写法或意图 | cocotb 2.1.0 写法 | 注意事项 |
|---|---|---|
from cocotb.binary import BinaryValue | from cocotb.types import LogicArray | 先删除旧模块导入 |
BinaryValue(..., binaryString="1010") | LogicArray("1010") | 位宽由字符串长度确定 |
| 给 DUT 赋整数值 | dut.data.value = 10 | 信号位宽仍由 HDL 决定 |
| 读取二进制文本 | str(dut.data.value) | 不丢前导零,可显示 X/Z |
| 读取无符号整数 | int(dut.data.value) | 仅适用于全部位已解析为 0/1 |
并不是每次赋值都必须显式构造对象。测试激励本来就是十进制数时,直接写 dut.data.value = 10 更清楚;需要按位拼接、描述掩码或保留固定宽度文本时,LogicArray("00001010") 更能表达意图。反过来,读取后要做的是位级比较就用字符串或位索引,要做计数、地址计算才转整数。把用途分清,比在全项目里寻找一种“万能替换格式”更可靠。
binaryString 与 binstr 也不是一对可以原样搬走的构造参数和属性:前者属于旧构造方式,应改为位置字符串参数;后者当前只是兼容入口,应改为 str()。尤其不要把 int(str(value), 2) 当通用兜底——它遇到 X/Z 同样会失败,还额外绕了一次文本转换。直接用值对象提供的转换接口,错误语义更明确。
三、新版最小 testbench 与通过证据
下面的回归先写入一个已解析的 8 位值,再分别验证文本和整数;随后故意写入含 X 的值,捕获 int() 的异常。预期异常被测试代码接住,因此整个用例应当通过,而不是靠忽略失败获得绿灯。
import cocotb
from cocotb.triggers import Timer
from cocotb.types import LogicArray
@cocotb.test()
async def logic_array_boundaries(dut):
dut.data.value = LogicArray("00001010")
await Timer(1, unit="ns")
value = dut.data.value
assert str(value) == "00001010"
assert int(value) == 10
dut.data.value = LogicArray("000010X1")
await Timer(1, unit="ns")
try:
int(dut.data.value)
except ValueError:
pass
else:
raise AssertionError("int() must reject X/Z values")实际输出中的类型、转换结果与回归摘要如下:
0.00ns INFO cocotb.regression running modern_test.logic_array_boundaries (1/1)
1.00ns INFO cocotb.counter resolved type=LogicArray str=00001010 int=10
1.00ns INFO cocotb.counter binstr compatibility=00001010 warnings=1
2.00ns INFO cocotb.counter unresolved str=000010X1
2.00ns INFO cocotb.counter int rejected: Can't convert LogicArray to int: it contains non-0/1 values
2.00ns INFO cocotb.regression modern_test.logic_array_boundaries passed
** TESTS=1 PASS=1 FAIL=0 SKIP=0 2.00 0.00 1236.06 **
MODERN_EXIT=0这段结果同时确认两点:dut.data.value 读出的是 LogicArray,而含 X 时 int() 抛的是 ValueError,不是断言失败或仿真器错误。最终 PASS=1 FAIL=0 与 MODERN_EXIT=0 才是修复完成的判据。
四、binstr 并非同义替换,int 也不能吞掉未知位
本环境实测 value.binstr 仍能返回 00001010,所以不能说这个属性在 2.1.0 已被彻底删除;但访问它会产生一条 DeprecationWarning,提示改用 str(value)。临时兼容旧代码时可以工作,新代码不应继续扩大它的使用。这里的结论只绑定本次实测版本,不外推为所有 cocotb 版本的共同行为。
整数转换则要守住数据有效性边界。X 表示未知,Z 表示高阻,它们不是可以随意当成 0 的数值。若为了消除异常而先做字符串替换,很可能把未初始化、总线冲突或连接错误藏成一个看似正常的整数。更稳妥的顺序是:复位并等待有效时刻;用 value.is_resolvable 判断能否转换;不可解析时打印 str(value) 并修复设计或采样时序。
一个适合放进真实测试的读取辅助逻辑如下。它把“值尚未有效”变成可读断言,而不是让测试在更深的位置偶然失败:
value = dut.data.value
assert value.is_resolvable, f"data contains X/Z: {str(value)}"
count = int(value)如果这里失败,下一步不是修改转换函数,而是沿仿真时间往回查。寄存器上电为 X,先确认复位是否真正到达、极性是否正确以及是否等待了有效时钟沿;总线出现 Z,再检查三态端口是否在预期时刻由某一方驱动。只有多个驱动冲突时,单纯延长等待通常无济于事。字符串里的未知位因此是设计状态证据,不应被当成格式噪音。
弃用警告也值得纳入迁移验收。本文用 warnings.catch_warnings() 主动捕获,实测一次 .binstr 访问对应一条警告。普通运行可能因 Python 的警告过滤规则而不显示它,“终端没打印”不等于 API 没有弃用。迁移测试可以临时打开警告记录,确认新路径只使用 str(),但不要为了让日志安静而全局屏蔽所有弃用警告,那会连带遮住其他升级问题。
| 信号内容 | str(value) | int(value) | 建议 |
|---|---|---|---|
00001010 | 00001010 | 10 | 可正常做数值断言 |
000010X1 | 000010X1 | ValueError | 查初始化、驱动与采样时刻 |
含 Z 的总线 | 保留 Z | ValueError | 查三态释放与多驱动 |
迁移完成后,全局搜索 cocotb.binary、BinaryValue、binaryString 和 .binstr 四个标记,再运行真实仿真。只让 Python 文件通过语法检查不够,因为信号值类型、四态解析与 HDL 位宽都要等仿真启动后才能验证。
代码获取:完整计数器 RTL、旧/新 testbench、Makefile 与一键脚本位于公开仓
experiments/ts-cocotb-binaryvalue-binstr/。准备好文中 Docker 环境后执行bash run.sh,脚本要求旧用例失败、新用例通过。
这一篇在知识体系里的位置
本篇处理的是 cocotb 测试代码中的值对象迁移,属于仿真验证的 API 兼容问题。cocotb no tests to run 排查关注测试模块名与 HDL 顶层名的配置,虽然两者都可能在零时刻停止,但一个查 Python 导入栈中的具体文件行,另一个查 MODULE 与 TOPLEVEL,修复面不同。
实验环境
- 机器/系统:Linux x86_64 Docker 容器 / Ubuntu 24.04.5 LTS
- 工具版本:cocotb 2.1.0、Python 3.12.12、Icarus Verilog 12.0 (stable)
- 日期:2026-10-31
- 完整命令:
bash run.sh总结
cocotb BinaryValue 用法报错在 2.1.0 中的根因是 cocotb.binary 已无法导入;删除旧类,用 LogicArray 构造位向量,通过 dut.signal.value 读写,并以 str() 保留四态文本、只在全 0/1 时用 int()。.binstr 本次仍可读取但已弃用;最终以含未知位边界测试的 PASS=1 FAIL=0 验证迁移。