Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Integrated Test

Integrated test can be marked by #[test(test_name)] attribute. The marked block will be identified as test, and executed through veryl test command.

There are three ways to describe integrated test:

  • Native test
  • SystemVerilog test
  • cocotb test

Native test uses Veryl’s built-in simulator, while SystemVerilog test and cocotb test use an external RTL simulator. About external RTL simulators used by veryl test, see Simulator. For all test types, if --wave option is specified, waveforms are generated.

Tests can be skipped by adding #[ignore] attribute. Ignored tests are not executed by default, but can be run with --ignored option. --include-ignored option runs both normal and ignored tests.

Test-only code paths can be enabled by combining the #[ifdef]/#[ifndef] attribute with names defined for the test. Names can be defined through the [test].defines field of Veryl.toml, or by the --define NAME (-D NAME) option of veryl test. The two sources are merged, and for SystemVerilog and cocotb tests, the same names are also passed to the external simulator.

#[test(test_ignored)]
#[ignore]
module test_ignored {
    inst clk: $tb::clock_gen;
    inst rst: $tb::reset_gen ( clk );

    initial {
        rst.assert();
        $finish();
    }
}

Native test

Native test allows writing testbenches directly in Veryl without embedding SystemVerilog or using external frameworks. A module with #[test(test_name)] attribute (without embed declaration) is treated as a native test.

The following testbench components are available:

  • $tb::clock_gen — clock signal generator (with optional #(period: N) parameter)
  • $tb::reset_gen — reset signal generator (with optional #(cycles: N) parameter)
  • $tb::file — file handle for writing output files
  • $tb::random — random-number generator (with the value type as a generic argument)

In addition to the built-in components, verification components written in Rust can be used through the $comp namespace. See Using a Component.

And the following system functions can be used in initial blocks:

  • $assert(condition), $assert(condition, format, args...) — check an assertion during simulation. On failure the simulation stops immediately and the test is reported as failed. format is a format string in the same style as $display, and args provides values formatted into it.
  • $assert_continue(condition), $assert_continue(condition, format, args...) — same as $assert but the simulation continues after a failure, so multiple failures can be collected in a single run. The test is still reported as failed.
  • $finish() — terminate simulation

Basic example

module Counter (
    clk: input  clock    ,
    rst: input  reset    ,
    cnt: output logic<32>,
) {
    always_ff {
        if_reset {
            cnt = 0;
        } else {
            cnt += 1;
        }
    }
}

#[test(test_counter)]
module test_counter {
    inst clk: $tb::clock_gen;
    inst rst: $tb::reset_gen ( clk );

    var cnt: logic<32>;

    inst dut: Counter (
        clk: clk,
        rst: rst,
        cnt: cnt,
    );

    initial {
        rst.assert();
        clk.next(10);
        $assert(cnt == 32'd10);
        $finish();
    }
}

Testbench methods

clock_gen provides the next method to advance clock cycles:

  • clk.next() — advance clock by 1 cycle
  • clk.next(N) — advance clock by N cycles

reset_gen provides the assert method to assert reset:

  • rst.assert() — assert reset synchronized to clock
  • rst.assert(duration) — assert reset for specified duration

The reset duration can also be configured at instantiation:

#[test(test_reset_cycles_param)]
module test_reset_cycles_param {
    inst clk: $tb::clock_gen;
    inst rst: $tb::reset_gen #( cycles: 5 ) ( clk );

    // ...

    initial {
        rst.assert();
        // ...
    }
}

Function calls in testbench

Testbench methods like clk.next can be called from user-defined functions:

#[test(test_function_call)]
module test_function_call {
    inst clk: $tb::clock_gen;
    inst rst: $tb::reset_gen ( clk );

    var cnt: logic<32>;

    // inst dut: Counter (clk, rst, cnt);

    function step_n (
        n: input logic<32>,
    ) {
        clk.next(n);
    }

    initial {
        rst.assert();
        step_n(5);
        step_n(5);
        $assert(cnt == 32'd10);
        $finish();
    }
}

Hierarchical reference

Signals inside the DUT can be read from an initial block through a hierarchical path. The path starts at an instance of the test module, and goes through nested instances by .. Internal signals don’t have to be routed to the top level for observation.

module Sub (
    clk: input clock   ,
    rst: input reset   ,
    din: input logic<4>,
) {
    var internal_reg: logic<4>;
    always_ff {
        if_reset {
            internal_reg = 0;
        } else {
            internal_reg = din + 1;
        }
    }
}

module Top (
    clk: input clock   ,
    rst: input reset   ,
    din: input logic<4>,
) {
    inst u_sub: Sub ( clk, rst, din );
}

#[test(test_hier)]
module test_hier {
    inst clk: $tb::clock_gen;
    inst rst: $tb::reset_gen ( clk );

    var din: logic<4>;

    inst dut: Top ( clk, rst, din );

    initial {
        rst.assert();
        din = 4'b0001;
        clk.next();
        $assert(dut.u_sub.internal_reg == 4'h2, "unexpected value");
        $display("internal_reg = %h", dut.u_sub.internal_reg);
        $finish();
    }
}

The referenced value can be used like any other expression, such as an argument of $assert and $display, a condition of if, and an operand with bit select.

There are the following restrictions:

  • A hierarchical reference is available only in initial blocks of a test module. Using it in RTL context like always_comb, or in a module which is not a test module, is reported as invisible_identifier.
  • It can’t be used in a function because a function body is shared with RTL callers.
  • An instance array can’t be a part of a hierarchical path.

A hierarchical reference is not counted as a reference to the signal. So a signal which is read only through a hierarchical reference is reported as unused_variable. It can be suppressed by the #[allow(unused_variable)] attribute.

File output

$tb::file is a file handle for writing output files during a native test. Unlike clock_gen and reset_gen, it is declared with var, then opened, written, and closed inside an initial block:

  • f.open(name) — open file name for writing, truncating any existing content
  • f.append(name) — open file name for writing, appending to any existing content
  • f.write(format, args...) — write formatted text, using the same format style as $display
  • f.flush() — flush buffered output to disk
  • f.close() — close the file
#[test(test_file)]
module test_file {
    var f: $tb::file;

    initial {
        f.open("out.txt");
        f.write("hex=%h dec=%d\n", 8'hAB, 8'd42);
        f.close();

        f.append("out.txt");
        f.write("appended\n");
        f.flush();
        f.close();

        $finish();
    }
}

Like other $tb::* components, $tb::file can only be used inside a #[test] module.

Random number generation

$tb::random is a random-number generator for native tests. The value type is given as a generic argument at declaration. It must be a 2-state integer type of at most 64 bits — for example u8u64, i8i64, bbool, or a bit<N> with N up to 64. 4-state types (logic / lbool), floating-point types, and widths over 64 bits are rejected.

It is declared with var, then used inside an initial block:

  • r.seed(value) — set the seed
  • r.get() — return a uniform random value over the full range of the element type
  • r.get_range(min, max) — return a uniform random value in the inclusive range min..=max
  • r.get_seed() — return the current seed
#[test(test_random)]
module test_random {
    var r: $tb::random::<u32>;
    var x: u32               ;

    initial {
        r.seed(42);
        x = r.get();
        x = r.get_range(0, 99);
        $finish();
    }
}

A specific bit width is used by binding it to a type first (a bit<N> cannot be written directly as a generic argument):

#[test(test_random_width)]
module test_random_width {
    gen my_t: type                = bit<12>;
    var r   : $tb::random::<my_t>;
    var x   : my_t               ;

    initial {
        x = r.get(); // 12-bit random
        $finish();
    }
}

When neither --seed nor [test].seed is specified, each generator is seeded from a run-wide random base seed, so a run is not reproducible across invocations. Passing an explicit seed (via --seed, [test].seed, or r.seed(...)) makes the generated sequence reproducible. Like other $tb::* components, $tb::random can only be used inside a #[test] module.

SystemVerilog test

SystemVerilog test can be described with inline specifier. The top level module of the block must have the same name as the test name.

The messages through $info, $warning, $error and $fatal system function are handled by Veryl compiler, and shown as exectution log. The calls of $error and $fatal are treated as test failure.

The following example, a SystemVerilog source code embedded by embed declaration are marked as test.

#[test(test1)]
embed (inline) sv{{{
    module test1;
        initial begin
            assert (0) else $error("error");
        end
    endmodule
}}}

cocotb test

cocotb test can be described with cocotb specifier. The target module name for test should be specified by the second argument of #[test] attribute.

#[test(test1, ModuleA)]
embed (cocotb) py{{{
    # cocotb code
}}}