Advanced Generators

For complicated testbench setups, you might want to reuse certain sequences from a macro.

The PEP380 yield from construct makes this look nicer:

In [1]:
from myirl import *
import myirl.simulation as sim
from myirl import targets

def toggle(p, m):
    yield [m.set(False)]        
    for i in range(4):
        yield [ p.set(~p), sim.wait('20 ns') ]
        
def data(clk, d, p, m):
    a = 1
    yield [m.set(True)]        
    for i in range(8):
        yield [ sim.wait(clk.posedge), p.set((d & a) != 0) ]
        a <<= 1
In [2]:
@block
def tb():
    p = Signal(bool(), name='p')
    marker = Signal(bool(), name = 'mark')
    
    d = Signal(intbv()[8:], name = 'data')
    clk = ClkSignal(name='clk')
    
    @bulk_delay(5)
    def clkgen():
        yield [
            clk.set(~clk)
        ]
    
    @sim.generator
    def seq():
        yield [
            p.set(False), sim.wait('5 ns'), marker.set(False), sim.wait(clk.posedge),
            marker.set(True), sim.wait(clk.posedge)
        ]
        yield from toggle(p, marker)
        yield from data(clk, 0xa5, p, marker)
        yield from toggle(p, marker)
    
        yield [ sim.raise_(sim.StopSimulation)]
    return [clkgen, seq]
In [3]:
from myirl.test.common_test import run_ghdl

def test():
    t = tb()
    f = t.elab(targets.VHDL)
    run_ghdl(f, tb, vcdfile="/tmp/tb1.vcd", debug = False)
    return t
In [4]:
t = test()
Creating sequential 'tb/seq' 
 Writing 'tb' to file /tmp/myirl_top_tb_sjugsmpj/tb.vhdl 

Waveform

In [5]:
import wavedraw; import nbwavedrom
TB = t.name;

waveform = wavedraw.vcd2wave("/tmp/tb1.vcd", TB + '.clk', None)
nbwavedrom.draw(waveform)

Bidirectional generator function

The yield from mechanisms can be taken further to generate testbenches from existing hardware descriptions. Here, a simple 'software-style' generator FSM is constructed that reacts to a value sent to it via (yield):

In [6]:
from collections import namedtuple

t_state = namedtuple('state', ['A', 'B', 'C', 'END'])
enum = t_state(*range(4))

def fsm(p, m):
    print("STARTED")
    
    state = enum.A

    while True:
        if state == enum.A:
            print("State A")
            b = (yield)
            if b == 1:
                state = enum.B
                yield [m.set(True)]
            elif b == 0:
                pass
            else:
                raise ValueError("Value can not be", b)

        elif state == enum.B:
            print("State B")
            b = (yield)
            if b == 1:
                state = enum.C    
                yield [m.set(False)]
            elif b == 0:
                pass
            else:
                raise ValueError("Value can not be", b)

        elif state == enum.C:
            print("State C")
            b = (yield)
            if b == 1:
                for i in range(8):
                    # TOGGLING (see below)
                    yield [ p.set(~p) ]
                state = enum.A
            elif b == 0:
                state = enum.END
                yield [ sim.wait('40 ns'), p.set(False), m.set(True) ]
            else:
                raise ValueError("Value can not be", b)

        elif state == enum.END:
            print("STATE END")
            break
        else:
            raise ValueError("Bad state")

The wrapper only translates by yield from:

In [7]:
def wrapper(routine):
    print("CALL WRAPPER")
    yield from routine
In [8]:
@block
def tb():
    p = Signal(bool(), name='p')
    marker = Signal(bool(), name = 'mark')
    
    d = Signal(intbv()[8:], name = 'data')
    clk = ClkSignal(name='clk')
    
    @bulk_delay(5)
    def clkgen():
        yield [
            clk.set(~clk)
        ]
    
    @sim.generator
    def seq():
        yield [
            p.set(False), sim.wait('10 ns'), marker.set(True), sim.wait(clk.posedge),
            marker.set(False), sim.wait(clk.posedge)
        ]
        # Construct wrapper for FSM:
        w = wrapper(fsm(p, marker))
        # Prime it:
        w.send(None)
        X = None # Don't care, allowed only during the 'TOGGLING' sequence
        for b in [0, 0, 1, 0, 0, 1, 1, 1, X, X, X, X, X, X, X, X, 0, 1, 1, 1, 0, 0, 0]:
            try:
                it = w.send(b)
#                 print("#", it)
                if it is not None:
                    yield it
                yield [sim.wait(clk.posedge)]
            except StopIteration:
                break

        yield [ sim.wait('50 ns'), sim.raise_(sim.StopSimulation)]
    return [clkgen, seq]
In [9]:
from myirl.test.common_test import run_ghdl

def test():
    t = tb()
    f = t.elab(targets.VHDL)
    run_ghdl(f, tb, vcdfile="/tmp/tb2.vcd", debug = False)
    return t
test()
Creating sequential 'tb/seq' 
CALL WRAPPER
STARTED
State A
State A
State A
State B
State B
State C
State A
State A
State B
State C
STATE END
 Writing 'tb' to file /tmp/myirl_top_tb_fy_1x4gz/tb.vhdl 
Out[9]:
[Instance tb I/F: [// ID: tb_0 to tb]]
In [10]:
TB = tb.name;

waveform = wavedraw.vcd2wave("/tmp/tb2.vcd", TB + '.clk', None)
nbwavedrom.draw(waveform)

This may appear extra complicated, but helps a lot with verification of known models against a hardware implementation. Also, it allows to loop in other co-simulation objects/pipes for step-wise verification.

In [11]:
# !cat {tb.ctx.path_prefix}/tb.vhdl