跳转到主要内容

用于测试硬件的Python包(magma生态系统的一部分)

项目描述

Fault

Linux Test MacOS Test BuildKite Status Code Coverage License

一个用于测试硬件的Python包(magma生态系统的一部分)。

API文档

变更日志

安装

pip install fault

文档

查看故障教程

支持的仿真器

示例

这里是一个在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_regConfigReg 中定义为 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 是基于输入 I0I1 的(而不是在 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 支持在使用 verilatorsystem-verilog/ncsim 目标时生成 .vcd 导出。

对于 verilator 目标,请使用 flags 关键字参数传递 --trace 标志。例如

tester.compile_and_run("verilator", flags=["-Wno-fatal", "--trace"])

--trace 标志必须传递给 verilator,以便它生成支持波形转储的代码。由 fault 生成的测试平台将包括调用 evalstep 时调用 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");

在生成的测试平台内部。

我如何将标志传递给模拟器?

verilatorsystem-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() 来断言所有序列都已处理完成,否则测试可能会在没有完成序列的情况下通过。

项目详情


发布历史 发布通知 | RSS 源

下载文件

下载适用于您平台的应用程序文件。如果您不确定选择哪个,请了解有关 安装包 的更多信息。

源分发

fault-4.0.1.tar.gz (117.5 kB 查看哈希值)

上传时间

由以下机构支持

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误日志 StatusPage StatusPage 状态页面