Getting started: Come scrivere un testbench con MyHDL?

Quando puoi misurare ciò di cui stai parlando, ed esprimerlo in numeri, tu conosci qualcosa su di esso; ma quando non puoi misurarlo, quando non puoi esprimerlo in numeri, la tua conoscenza è scarsa e insoddisfacente:
può essere l’inizio della conoscenza, ma nei tuoi pensieri, sei avanzato poco sulla via della scienza.
– William Thomson, Lord Kelvin (1824-1907)

Una volta realizzata la descrizione in Python del nostro Multiplexer , dobbiamo testare se funziona o meno. Infatti non è sufficiente descriverne il comportamento interno (architecture) e l’interfaccia esterna (design entity) perché il solo modo per verificare che funzioni è di simularlo. In questo articolo avevamo già visto come farlo con ISim ora però non ci resta che farlo in Python sempre con il modulo MyHDL e MyHDL Peek che ci permette la visualizzazione su un Notebook Jupyter dell’andamento dei segnali attraverso le forme d’onda ottenute dalla simulazione.

from myhdlpeek import Peeker 
from random import randrange
from myhdl import *

@block
def mux(s, o, I0, I1):

    """
    MUX 2:1
	I0,I1,s: inputs 
	O: output
    """

    @always_comb
    def logic():
        if s == 0:   # se s=0 in output avremo I0
            o.next = I0
        else:
            o.next = I1 #se = 1 in output avremo I1
    return logic

Tech Stack

[1] Installazione del modulo open source MyHDL
[2] Installazione MyHDL Peek
[3] Utilizzo di Python 3.5 installato attraverso Anaconda per Windows
[4] Utilizzo di un notebook Jupyter

Installazione MyHDL Peek

MyHDL Peek è un modulo Python sviluppato da Dave Vandenbout che ci permette di visualizzare il diagramma temporale dei segnali su un notebook Jupyter. Apportare modifiche alla nostra descrizione in Python con MyHDL e visualizzare immediatamente il risultato.
Per installare la versione 0.0.5 di myhdlpeek ci basta digitare:

pip install myhdlpeek




Testbench

La simulazione è una procedura di testing utilizzata per garantire che il circuito da sintetizzare implementi il comportamento previsto.
La procedura è composta dall’istanziazione del modulo da testare (UUT, unity under test) e dagli stimoli da applicare alla UUT.

Infine osserviamo la risposta dell’UUT visualizzando la waveform dell’uscita utilizzando MyHDL Peek.

def testBench():

    # inizializzazione dei valori
    o, I0,I1 = [Signal(intbv(0)) for i in range(3)] # Integer per i segnali in entrata
    s = Signal(bool(0)) # Binary signal per il selettore
    
    #istanziazione del modulo da testare
    uut = mux(s, o, I0, I1)

    Peeker.clear()         
    Peeker(I0, 'I0')         
    Peeker(I1, 'I1')         
    Peeker(o, 'O')        
    Peeker(s, 'select')

    @instance
    def stimulus():
        print("t s I0 I1 o")
        for i in range(8): # 8 volte 
            I0.next, I1.next = randrange(8), randrange(8)
            s.next =randrange(2)
            yield delay(2) #utilizzato alla pari di wait
            print("%d %s %s %s %s" % (now(), s, I0, I1,o))

            
    return (uut, stimulus)




Inizializzazione della simulazione

Dobbiamo assegnare ai segnali in input dei valori iniziali in base al loro tipo:

– tre ingressi { I }_{ 0 } , { I }_{ 1 } , { S } di tipo integer.
– uscita { O } di tipo booleano.

o, I0,I1 = [Signal(intbv(0)) for i in range(3)] # Integer per i segnali in entrata
s = Signal(bool(0)) # Binary signal per il selettore




Generazione degli stimoli

Quando abbiamo testato il MUX con ISim abbiamo scritto all’interno del process body una serie di assegnamenti tra segnali e di istruzioni wait per fornire gli stimoli:

-- Stimulus process
   stim_proc: process
   begin		     
    I0 <= '1';  -- assegnamenti
    I1 <= '0';
    s <= '0'; 
    wait for 20 ns;
    s <= '1'; 
    wait for 20 ns;	 
    
    I0 <= '0'; 
    I1 <= '1';
    s <= '0'; 
    wait for 20 ns;
    
    s <= '1'; 
    wait for 20 ns;			
   end process;




Allo stesso modo con MyHDL creeremo un generatore stimulus() per poi assegnare una serie di valori casuali ai nostri segnali in ingresso:

@instance
    def stimulus():
        print("t s I0 I1 o")
        for i in range(8): # 8 volte 
            I0.next, I1.next = randrange(8), randrange(8)
            s.next =randrange(2)
            yield delay(2) #utilizzato alla pari di wait
            print("%d %s %s %s %s" % (now(), s, I0, I1,o))




Per farlo utilizzeremo semplicemente il metodo randrange() che restituisce un elemento casuale da range(start,stop,step). Ricordandoci che il selettore è a 1 bit e che può quindi acquisire solo due valori randrange(2)
L’istruzione yield in MyHDL  viene impiegata allo stesso modo che wait in VHDL per sospendere l’esecuzione del generatore specificando la condizione che deve verificarsi perché venga ripreso. In questo caso un ritardo della propagazione del segnale con delay().

Simulation

# Simulate the uut, stimulus e peekers.
sim = Simulation(uut, stimulus, *Peeker.instances()).run()




Output waveform

Per un componente di bassa complessità come un Multiplexer per capire se si comporta in modo corretto possiamo analizzare la waveform prodotta dalla simulazione. Innanzitutto dobbiamo associare ad ogni segnale di I/O che dobbiamo monitorare un Peeker:

Peeker.clear()         # Clear any existing Peekers.
Peeker(I0, 'I0')         
Peeker(I1, 'I1')         
Peeker(o, 'O')         
Peeker(s, 'select')




Dal nostro taccuino Jupyter MyHDL Peek ci permette di visualizzare i risultati della simulazione con una comoda tabella:

signals = ('select I0 I1 O')
Peeker.to_html_table(signals)

Notiamo subito che il selettore S assume come valori 1 o 0  e che { I }_{ 0 } e { I }_{ 1 } hanno valori casuali.
Dal comportamento del Multiplexer sappiamo che se  il selettore assume  S=0 in uscita o avremo il valore di { I }_{ 0 } e viceversa se  S=1 l’uscita sarà { I }_{ 1 }.  

Per esempio all’istante t=2 con S=0 abbiamo come valore in uscita quello di { I }_{ 0 }.

Prendiamo t=4 con S=1 in uscita abbiamo come ci aspettavamo{ I }_{ 1 }.
Ora non ci resta che controllare il diagramma temporale della simulazione

signals = ('select I0 I1 O')
Peeker.to_wavedrom(signals, start_time=0, stop_time=14, tock=True,title='Output waveform' )





Codice completo

from myhdlpeek import Peeker 
from random import randrange
from myhdl import *

@block
def mux(s, o, I0, I1):

    """
    MUX 2:1
    I0,I1,s inputs 
    O output

    """

    @always_comb
    def logic():
        if s == 0:   # se s=0 in output avremo I0
            o.next = I0
        else:
            o.next = I1 #se = 1 in output avremo I1
    return logic

def testBench():


    # inizializzazione 
    o, I0,I1 = [Signal(intbv(0)) for i in range(3)] # Integer per i segnali in entrata
    s = Signal(bool(0)) # Binary signal per il selettore
    
    uut = mux(s, o, I0, I1)

    Peeker.clear()         
    Peeker(I0, 'I0')         
    Peeker(I1, 'I1')         
    Peeker(o, 'O')        
    Peeker(s, 'select')

    @instance
    def stimulus():
        print("t s I0 I1 o")
        for i in range(8): # 8 volte 
            I0.next, I1.next = randrange(8), randrange(8)
            s.next =randrange(2)
            yield delay(2) #utilizzato alla pari di wait
            print("%d %s %s %s %s" % (now(), s, I0, I1,o))

            
    return (uut, stimulus)

uut, stimulus = testBench()

# Simulate mux,uut e Peekers
sim = Simulation(uut, stimulus, *Peeker.instances()).run()

# Visualizzabile solo con taccuino Jupyter
Peeker.to_wavedrom()




Bibliografia

Sitografia

MyHDL:documentazione
[1] Jan Decaluwe, MyHDL documentation, Release 1.0dev
[2] Dave Vandenbout, MyHDL Peek documentation, Release 0.0.5