用于测试硬件的Python包(magma生态系统的一部分)
项目描述
Fault
一个用于测试硬件的Python包(magma生态系统的一部分)。
安装
pip install fault
文档
查看故障教程
支持的仿真器
- 数字仿真
- 形式验证
- 由pySMT支持的引擎
- 模拟仿真
- 混合信号仿真
- Verilog-AMS via Cadence Incisive/Xcelium
示例
这里是一个在magma中定义的简单ALU。
import magma as m
class ConfigReg(m.Circuit):
io = m.IO(D=m.In(m.Bits[2]), Q=m.Out(m.Bits[2])) + \
m.ClockIO(has_ce=True)
reg = m.Register(m.Bits[2], has_enable=True)(name="conf_reg")
io.Q @= reg(io.D, CE=io.CE)
class SimpleALU(m.Circuit):
io = m.IO(
a=m.In(m.UInt[16]),
b=m.In(m.UInt[16]),
c=m.Out(m.UInt[16]),
config_data=m.In(m.Bits[2]),
config_en=m.In(m.Enable)
) + m.ClockIO()
opcode = ConfigReg(name="config_reg")(io.config_data, CE=io.config_en)
io.c @= m.mux(
[io.a + io.b, io.a - io.b, io.a * io.b, io.a ^ io.b], opcode)
这是一个使用配置接口的故障测试示例,它期望内部寄存器上的值,并检查执行预期操作的结果。
import operator
import fault
ops = [operator.add, operator.sub, operator.mul, operator.floordiv]
tester = fault.Tester(SimpleALU, SimpleALU.CLK)
tester.circuit.CLK = 0
tester.circuit.config_en = 1
for i in range(0, 4):
tester.circuit.config_data = i
tester.step(2)
tester.circuit.a = 3
tester.circuit.b = 2
tester.eval()
tester.circuit.c.expect(ops[i](3, 2))
我们可以使用三个不同的仿真器运行此测试
tester.compile_and_run("verilator", flags=["-Wno-fatal"], directory="build")
tester.compile_and_run("system-verilog", simulator="ncsim", directory="build")
tester.compile_and_run("system-verilog", simulator="vcs", directory="build")
处理内部信号
Fault支持查看、期望和打印内部信号。对于verilator
目标,您应该使用关键字参数magma_opts
并将"verilator_debug"
设置为true。这将导致coreir编译带有所需调试注释的verilog。示例
tester.compile_and_run("verilator", flags=["-Wno-fatal"],
magma_opts={"verilator_debug": True}, directory="build")
如果您正在使用来自 coreir
实现的 mantle.Register
,您还可以直接使用 value
字段来修改内部寄存器的值。请注意,conf_reg
在 ConfigReg
中定义为 mantle.Register
的一个实例,测试平台通过将 confg_reg.value
设置为 1
来访问它。
tester = fault.Tester(SimpleALU, SimpleALU.CLK)
tester.circuit.CLK = 0
# Set config_en to 0 so stepping the clock doesn't clobber the poked value
tester.circuit.config_en = 0
# Initialize
tester.step(2)
for i in reversed(range(4)):
tester.circuit.config_reg.conf_reg.value = i
tester.step(2)
tester.circuit.config_reg.conf_reg.O.expect(i)
# You can also print these internal signals using the getattr interface
tester.print("O=%d\n", tester.circuit.config_reg.conf_reg.O)
常见问题解答
我如何编写依赖于电路运行时状态的测试平台逻辑?
测试中的常见模式是仅根据电路的状态执行某些操作。例如,可能只想在有效信号为高时期望输出值,否则忽略它。另一种模式是通过循环结构随时间改变期望值。最后,可能需要期望一个值,它是其他运行时值的函数。为了支持这些模式,fault
提供了对“查看”值、对“查看”值执行表达式、if 语句和while 循环的支持。
查看表达式
假设我们有一个如下所示的电路
class BinaryOpCircuit(m.Circuit):
io = m.IO(I0=m.In(m.UInt[5]), I1=m.In(m.UInt[5]), O=m.Out(m.UInt[5]))
io.O @= io.I0 + io.I1 & (io.I1 - io.I0)
我们可以编写一个通用测试,它期望输出 O
是基于输入 I0
和 I1
的(而不是在 Python 中计算期望值)。
tester = fault.Tester(BinaryOpCircuit)
for _ in range(5):
tester.poke(tester._circuit.I0, hwtypes.BitVector.random(5))
tester.poke(tester._circuit.I1, hwtypes.BitVector.random(5))
tester.eval()
expected = tester.circuit.I0 + tester.circuit.I1
expected &= tester.circuit.I1 - tester.circuit.I0
tester.circuit.O.expect(expected)
这对于编写可重用测试组件非常有用(例如,将输出检查逻辑与各种输入激励生成器组合)。
控制结构
tester._while(<test>)
动作接受一个查看值或表达式作为循环的测试条件,并返回一个子测试器,允许用户向循环体中添加操作。以下是一个简单的示例,该示例在循环体中打印一些调试信息,直到完成信号被置位。
# Wait for loop to complete
loop = tester._while(dut.n_done)
debug_print(loop, dut)
loop.step()
loop.step()
# check final state
tester.circuit.count.expect(expected_num_cycles - 1)
请注意,您也可以在循环后添加操作来检查循环完成后期望的行为。
tester._if(<test>)
动作通过接受测试查看值或表达式并以条件执行操作,其行为类似。以下是一个简单的示例
if_tester = tester._if(tester.circuit.O == 0)
if_tester.circuit.I = 1
else_tester = if_tester._else()
else_tester.circuit.I = 0
tester.eval()
tester._for(<num_iter>)
动作提供了一个简单的循环固定迭代次数的方法。使用属性 index
来访问当前迭代,例如
loop = tester._for(8)
loop.poke(circ.I, loop.index)
loop.eval()
tester.expect(circ.O, loop.index)
我可以使用哪些 Python 值来修改/期望端口?
以下是用于修改以下端口类型的支持 Python 值
m.Bit
-bool
(True
/False
) 或int
(0
/1
) 或hwtypes.Bit
m.Bits[N]
-hwtypes.BitVector[N]
,int
(表示它的位数等于N
)m.SInt[N]
-hwtypes.SIntVector[N]
,int
(表示它的位数等于N
)m.UInt[N]
-hwtypes.UIntVector[N]
,int
(表示它的位数等于N
)m.Array[N, T]
-list
(其中列表长度等于N
,元素递归符合T
的支持值类型)。例如,假设我有一个类型为m.Array[3, m.Bits[3]]
的端口I
。我可以按如下方式修改它val = [random.randint(0, (1 << 4) - 1) for _ in range(3)] tester.poke(circ.I, val)
您也可以按元素修改它for i in range(3): val = random.randint(0, (1 << 4) - 1) tester.poke(circ.I[i], val) tester.eval() tester.expect(circ.O[i], val)
m.Tuple(a=m.Bits[4], b=m.Bits[4])
-tuple
(其中元组的长度等于字段的数量),dict
(其中键/值对与元组字段一一对应)。例如tester.circuit.I = (4, 2) tester.eval() tester.circuit.O.expect((4, 2)) tester.circuit.I = {"a": 4, "b": 2} tester.eval() tester.circuit.O.expect({"a": 4, "b": 2})
我如何使用 fault 生成波形?
Fault 支持在使用 verilator
和 system-verilog/ncsim
目标时生成 .vcd
导出。
对于 verilator
目标,请使用 flags
关键字参数传递 --trace
标志。例如
tester.compile_and_run("verilator", flags=["-Wno-fatal", "--trace"])
--trace
标志必须传递给 verilator,以便它生成支持波形转储的代码。由 fault 生成的测试平台将包括调用 eval
和 step
时调用 tracer->dump(main_time)
所需的逻辑。main_time
在每次调用 step 时递增。输出 .vcd
文件将保存在 logs/{circuit_name}
文件中,其中 circuit_name
是传递给 Tester
的电路名称。logs
目录将放在生成的平台相同的目录中,该目录由 directory
关键字参数控制(默认为 "build/"
)。
对于 system-verilog
目标,使用 compile_and_run
参数 dump_waveform=True
启用此功能。默认情况下,波形文件将命名为 waveforms.vcd
(用于 ncsim
)和 waveforms.vpd
(用于 vcs
)。可以使用参数 waveform_file="<file_name>"
修改文件名。
vcs
模拟器还支持使用 waveform_type="fsdb"
参数导出 fsdb
。为此,您还需要使用 flags
参数使用在您的 verdi 手册中定义的路径。例如,$VERDI_HOME/doc/linking_dumping.pdf
。
以下是一个使用较旧版本的 verdi 的示例(使用 VERDIHOME 环境变量)
verdi_home = os.environ["VERDIHOME"]
# You may need to change the 'vcs_latest' and 'LINUX64' parts of the path
# depending on your verdi version, please consult
# $VERDI_HOME/doc/linking_dumping.pdf
flags = ['-P',
f' {verdi_home}/share/PLI/vcs_latest/LINUX64/novas.tab',
f' {verdi_home}/share/PLI/vcs_latest/LINUX64/pli.a']
tester.compile_and_run(target="system-verilog", simulator="vcs",
waveform_type="fsdb", dump_waveforms=True, flags=flags)
以下是一个较新版本的 verdi 的示例
verdi_home = os.environ["VERDI_HOME"]
flags = ['-P',
f' {verdi_home}/share/PLI/VCS/linux64/novas.tab',
f' {verdi_home}/share/PLI/VCS/linux64/pli.a']
tester.compile_and_run(target="system-verilog", simulator="vcs",
waveform_type="fsdb", dump_waveforms=True, flags=flags)
要配置 fsdb 导出,请使用 compile_and_run 命令的 fsdb_dumpvars_args
参数传递一个字符串到 $fsdbDumpvars()
函数。
例如
tester.compile_and_run(target="system-verilog", simulator="vcs",
waveform_type="fsdb", dump_waveforms=True,
fsdb_dumpvars_args='0, "dut"')
将产生
$fsdbDumpvars(0, "dut");
在生成的测试平台内部。
我如何将标志传递给模拟器?
verilator
和 system-verilog
目标支持参数 flags
,它接受一个标志(字符串)列表,这些标志将通过模拟器命令(verilator 的 verilator,ncsim 的 irun,vcs 的 vcs,iverilog 的 iverilog)传递。
我可以在 expect 失败时包含一条消息吗?
使用 expect 动作的 msg
参数。您可以传递一个独立的字符串,例如
tester.circuit.O.expect(0, msg="my error message")
或者您可以使用 printf/$display 样式的消息通过一个元组传递。第一个参数应该是格式字符串,后续参数是格式值,例如
tester.circuit.O.expect(0, msg=("MY_MESSAGE: got %x, expected 0!",
tester.circuit.O))
我可以在测试平台中显示或打印值吗?
是的,您可以使用 tester.print
API,它接受一个格式字符串和任意数量的参数。以下是一个示例
tester = fault.Tester(circ, circ.CLK)
tester.poke(circ.I, 0)
tester.eval()
tester.expect(circ.O, 0)
tester.poke(circ.CLK, 0)
tester.step()
tester.print("%08x\n", circ.O)
我只需生成一个测试平台而不运行它吗?
是的,以下是一个示例
# compile the tester
tester.compile("verilator")
# generate the test bench file (returns the name of the file)
tb_file = tester.generate_test_bench("verilator")
或对于系统 verilog
tester.compile("system-verilog", simulator="ncsim")
tb_file = tester.generate_test_bench("system-verilog")
使用 ReadyValid 测试器
Fault 提供了一个 ReadyValidTester
,它提供了一些方便的特性,用于对具有序列的 ReadyValid
接口进行单元测试。
考虑以下电路
class Main2(m.Circuit):
io = m.IO(I=m.Consumer(m.ReadyValid[m.UInt[8]]),
O=m.Producer(m.ReadyValid[m.UInt[8]]),
inc=m.In(m.UInt[8]),
) + m.ClockIO()
count = m.Register(m.UInt[2])()
count.I @= count.O + 1
enable = io.I.valid & (count.O == 3) & io.O.ready
io.I.ready @= enable
io.O.data @= m.Register(m.UInt[8], has_enable=True)()(io.I.data + io.inc,
CE=enable)
io.O.valid @= enable
输出流 O
是输入流 I
加上 inc
的值,并延迟 4 个周期。
以下是一个简单的测试,它提供了输入序列 I
和期望的输出序列 O
def test_lifted_ready_valid_sequence_simple():
I = [BitVector.random(8) for _ in range(8)] + [0]
O = [0] + [i + 2 for i in I[:-1]]
tester = f.ReadyValidTester(Main2, {"I": I, "O": O})
tester.circuit.inc = 2
tester.finish_sequences()
tester.compile_and_run("verilator", disp_type="realtime")
请注意,我们以字典的形式提供序列,将端口名称映射到序列,在构造函数中。之后,我们可以在正常的测试器中自由地 poke
值,在这种情况下为 inc
提供 2
,这将满足提供的流。
注意:用户必须显式使用 tester.circuit
peek/poke 接口,或调用 tester.poke(tester._circuit, value)
,因为用户电路在内部被封装(不能调用 tester.poke(Main2, value)
)。
测试通过调用 tester.finish_sequences()
完成,这是一个便利的 API,它等待提供的序列完成。
以下是一个应该失败的上述测试的不同版本(在中途更改 inc
的值)。
def test_lifted_ready_valid_sequence_simple_fail():
I = [BitVector.random(8) for _ in range(8)] + [0]
O = [0] + [i + 2 for i in I[:-1]]
tester = f.ReadyValidTester(Main2, {"I": I, "O": O})
tester.circuit.inc = 2
# Should work for a few cycles
for i in range(9):
tester.advance_cycle()
# Bad inc should fail
tester.circuit.inc = 3
tester.finish_sequences()
with pytest.raises(AssertionError):
tester.compile_and_run("verilator", disp_type="realtime")
最后,这是一个更改 inc
值以匹配期望序列的变体。
def test_lifted_ready_valid_sequence_changing_inc():
I = [BitVector.random(8) for _ in range(8)] + [0]
O = [0] + [I[i] + ((i + 1) % 2) for i in range(8)]
tester = f.ReadyValidTester(Main2, {"I": I, "O": O})
# Sequence expects inc to change over time
for i in range(8):
tester.circuit.inc = i % 2
tester.advance_cycle()
tester.wait_until_high(tester.circuit.O.ready & tester.circuit.O.valid)
# Advance one cycle to finish last handshake
tester.advance_cycle()
tester.expect_sequences_finished()
tester.compile_and_run("verilator", disp_type="realtime")
注意: 测试结束时,我们调用 expect_sequences_finished()
来断言所有序列都已处理完成,否则测试可能会在没有完成序列的情况下通过。
项目详情
fault-4.0.1.tar.gz 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | d58d7a22c61f751ec8f348cdbcefe3d42a00075909d66518cab63cec6f792ed8 |
|
MD5 | 391cac176bdbc9222f3e0342cf7ab554 |
|
BLAKE2b-256 | b2e0d74ce7fcc84cd1df9ab3d061147c34ef11b9f96d7a1522fc8273f2bacdc6 |