The SystemVerilog code to test

Here we show an example that drives characters of a string into an 8-bit logic, which a SystemVerilog programmer can easily understand this code except the Nicotb VPI function calls. Besides, we shall illustrate later using Nicotb to detect the 6 characters right after the marker string "JUST", which is "MONIKA".

always #1 clk = ~clk;
initial begin
    clk = 0;
    rst = 1;
    character = 0;
    #1 $NicotbInit();
    #10 rst = 0;
    #10 rst = 1;
    #5
    for (int i = 0; i < LEN; i++) begin
        @(posedge clk)
        // assign characters = "AABABA__JUSTMONIKA__CDEDE";
        character <= characters[8*(LEN-i)-1 -: 8];
    end
    #100
    $NicotbFinal();
    $finish;
end

I don't intend to introduce the SystemVerilog part further, so I just explain $NicotbInit(), $NicotbFinal(). As their names suggest, you must $NicotbFinal() call it right before $finish, and call $NicotbInit() call it right after the initialization (preferably with little latency),

To co-simulate with Python, Python has to know about the timing of clock and reset, and this can be done by macros that defines two integer IDs rst_out, ck_ev. Whenever these events happen, these IDs are used to notify Python for such occurence.

`Pos(rst_out, rst)
// Roughly equivalent to:
//   integer rst_out;
//   always@(posedge rst) $NicotbTriggerEvent(rst_out);
`PosIf(ck_ev, clk, rst)

The Nicotb testbench in Python

Connect SystemVerilog signals

For connecting signals and events in Python, we need CreateEvents and CreateBus. The CreateEvents is easy and doesn't need much explanation; A bus is a set of signals, so CreateBus function call accepts a tuple type. For now our bus has only one signal, and ("", "character") means the character signal under the top-level module.

rst_out, clk = CreateEvents(["rst_out", "ck_ev"])
bus = CreateBus((
    ("", "character"),
))

After connecting the hardware parts, this following is the testbench logic, which is mostly standard Python codes. A main difference is yield, and you can view it as equivalence of @(posedge clk) in SystemVerilog.

To access the SystemVerilog signal value, an explicit bus.Read() is required to read the value from SystemVerilog through VPI. In this case, we call at every cycle, namely after yield clk. (Also, some simulators require special flags to let you access signals through VPI, such as +access+rw for ncverilog.) The signals of a bus can be accessed by the name like bus.character, and we provide several ways to access the signals, but this is the simplest one. A signal has two fields – value and x, which is exactly the 4-value encoding of VPI. If you are sure the signal is not x or z, then you can use only value. The value and x are all Numpy arrays, and if it is a scalar, its shape is (1,), which means you can access the value of character through bus.character.value[0] after calling bus.Read().

def main():
    pattern = [ord(c) for c in "JUST"]
    n_trail = 6
    match_idx = 0
    yield rst_out
    while True:
        yield clk
        bus.Read()
        if bus.character.value[0] == pattern[match_idx]:
            match_idx = match_idx+1
            if match_idx == len(pattern):
                break
        else:
            match_idx = 0
    s = str()
    for i in range(n_trail):
        yield clk
        bus.Read()
        s += chr(bus.character.value[0])
    print(s)

RegisterCoroutines([main()])  # this means main is an initial block

You can find this example in 0_pattern/ directory. Running this code by make -f ../Makefile tb, you can see the MONIKA is output during the simulation as desired. Similar example(s) are simple_test/, which can be run by the same command.