Poco più di un decennio fa erano in gran voga le metodologie Esl (Electronic system-level), basate su una serie di linguaggi che promettevano di elevare il livello di astrazione utilizzabile sia in sede di progettazione che di verifica, tra i quali quelli prevalenti erano C/C++, SystemC e SystemVerilog. Sebbene C e SystemC siano i linguaggi maggiormente utilizzati per la modellizzazione astratta dell’hardware e dei sistemi, spetta a SystemVerilog il merito di aver standardizzato le funzionalità necessarie per effettuare una verifica di tipo avanzato, come ad esempio la generazione di stimoli randomizzati e vincolati o la copertura funzionale. Nel frattempo, molti utenti si sono messi alla ricerca di modalità più efficienti per operare una descrizione dello stimolo, in particolare sotto forma di sistemi che fossero capaci di estendere la quantità degli scenari di verifica automatizzabili a partire da una unica descrizione compatta, e che fossero al contempo in grado anche di migliorare l’efficienza del processo di generazione. Questa inFact di Mentor ha messo a disposizione esattamente queste funzionalità, tramite un approccio basato su regole e grafi preso in prestito dalle esistenti tecniche di testing del software ed esteso per adattarlo alla verifica dell’hardware. Poiché tale modello basato su regole è del tutto indipendente dal linguaggio target - è stato applicato con almeno 7 distinti Hvl (Hardware verification language) - ha sempre costituito una soluzione adatta a implementare meccanismi di Portable Stimulus. Gli addetti alla Hls spesso affermano che uno dei principali benefici che si ottengono da una migrazione della progettazione verso il C++/SystemC è costituito dalle elevate performance della verifica, grazie alle quali si possono effettuare una quantità notevolmente maggiore di test. Tuttavia, all’interno dell’ambiente C non esistono degli standard per le funzionalità avanzate di verifica con potenza paragonabile a quelli presenti in SystemVerilog, che includono, tra le altre cose, la modellizzazione di stimoli randomizzati o automatizzati. Una soluzione per il Portable Stimulus deve fornire questo stesso tipo di potenza e di funzionalità, oltre alla capacità di preservare l’investimento effettuato per la creazione di un modello di stimolo in ambienti di simulazione basati sul linguaggio C, con la possibilità di estenderlo in avanti laddove l’Rtl venga incapsulato all’interno di codice SystemVerilog, e viceversa.
Il modello di stimolo comune
Il modello di stimolo basato su regole viene, come ci si può aspettare, creato gerarchicamente partendo da un file principale di regole top-level, che tipicamente non si discosta molto da un template di default, per proseguire con uno o più file modulari dedicati alle specifiche regole di diversi segmenti del codice. Il file di regole top-level dichiara il rule_graph principale, assegnandogli un nome, e potrebbe anche (a seconda dell’architettura del codice) non contenere altro se non le istruzioni necessarie per importare i diversi file relativi ai rule segment, che definiscono i dettagli dello stimolo da applicare. L’esempio illustrato in Fig. 1 mostra 4 file distinti, due dei quali - test_data_C.rules e test_data_SV.rules - definiscono entrambi un oggetto di tipo rule_graph (grafo di regole), denominato test_data_gen. Questi due file di regole top-level corrispondono a due componenti di tipo “grafo” che sono in realtà solo due distinti wrapper per uno stesso modello effettivo dello stimolo, ognuno dei quali relativo ad uno specifico linguaggio. In altre parole, i motori di generazione automatizzati di inFact creeranno sia una classe C++ che una classe SV, denominate rispettivamente test_data_C e test_data_SV, ognuna delle quali definirà con la propria sintassi un modello dell’unico grafo di regole denominato test_data_gen. Entrambi i file di regole top-level importano una gerarchia comune di file rule segment, che definiscono il comportamento effettivo. Poiché tutte le definizioni della gerarchia di regole vengono mantenute all’interno di file comuni, i modelli compilati dei grafi si comporteranno in modo identico. Il file test_data_C.rules contiene un costrutto aggiuntivo, costituito da un attributo che specifica alcuni requisiti peculiari del linguaggio C, relativi al codice da generare. In questo caso, contiene il codice necessario per aggiungere una istruzione include all’interno del file C++ che verrà generato, per la definizione della classe. Il linguaggio supporta svariati altri attributi che possono essere utilizzati per customizzare i file Hvl generati, ma nessuno di essi produce impatti sulla struttura del sottostante modello del grafo. Il file test_data_gen.rseg definisce la regola per lo scenario (o gli scenari) che il grafo può generare, che in questo caso consiste semplicemente nel ciclare ripetutamente attraverso la randomizzazione dei contenuti dell’oggetto test_data. Lo stesso oggetto test_data, dichiarato come una struct nel linguaggio delle regole di inFact, è stato definito all’interno di un distinto file rule segment, per favorire la modularità ed il riutilizzo del codice. Questa struct è internamente ulteriormente strutturata in modo gerarchico, poiché definisce altre struct denominate packedArray0 e packedArray1, che riflettono delle struct C++ definite e utilizzate per lo stimolo del Dut all’interno del testbench C++. Quello appena menzionato è un altro elemento chiave della metodologia, vale a dire il fatto che il grafo delle regole referenzi oggetti aventi lo stesso nome e gerarchia, e utilizzi tipi di dati che possono essere mappati su corrispondenti tipi dei linguaggi C++ e SV. Il linguaggio di inFact permette di definire la larghezza in bit per ogni singola variabile, il che consente il pieno utilizzo sia dei tipi dati algoritmici di Mentor, aventi precisione a livello del bit, sia tutti i tipi dati del SystemC. In questo esempio, l’aspetto dell’oggetto relativo ai dati di test deriva da un testbench C++ incentrato su un modello C che implementa un moltiplicatore-accumulatore vettoriale configurabile. Il primo step per l’implementazione di questa metodologia consiste, dunque, nel determinare quali degli input del Dut dovranno essere randomizzati dal modello del grafo, quali siano le loro dimensioni in bit, e nel raccogliere tali valori all’interno di una struct o di una classe dedicata ai dati di test. In questo caso, il packedArray0 contiene un array di dimensione fissa, pari ad 8 elementi di valori larghi 10 bit ciascuno, mentre il packedArray1 contiene un array simile, di valori larghi 7 bit. In aggiunta a questi, vi è un singolo valore di 4 bit denominato num_col. I tipi di dati utilizzati per queste struct sono stati definiti utilizzando i tipi di dati presenti in AlgorithmicC di Mentor che, grazie alla precisione al singolo bit, consentono di modellizzare i progetti con un livello arbitrario di precisione. Sebbene la descrizione sia iniziata illustrando il testbench C++, dovrà essere presente anche un analogo oggetto definito nel dominio SystemVerilog. Le versioni SystemVerilog e C++ di questo oggetto sono illustrate in Figura 3. Il modello SystemVerilog, come il modello inFact, può contenere dei vincoli di tipo algebrico, anzi sarebbe probabilmente costretto a farlo, se la sua randomizzazione dovesse avvenire tramite una tradizionale chiamata alla funzione .randomize() di SystemVerilog. Se tuttavia la randomizzazione verrà sempre effettuata da parte del grafo inFact, ciò non è necessario.
Esecuzione all’interno di un testbench C
Una volta definito l’oggetto destinato a contenere i dati di test, l’integrazione del modello di portable stimulus all’interno del testbench C è piuttosto semplice. Come accennato in precedenza, dal modello comune di regole viene automaticamente creata una classe C++, e questa classe possiede un metodo predefinito derivante da una interfaccia definita all’interno del linguaggio delle regole. Il file test_data_gen.rseg dichiara una interfaccia denominata fill, che opera su ogni istanza del tipo test_data. Ciò comporta la produzione di un metodo, di un task oppure di una funzione all’interno dell’oggetto HVL generato, avente nome ifc_fill (con la semplice aggiunta del prefisso ifc_). Tale metodo, task o funzione accetterà un argomento che è un handle al corrispondente oggetto Hvl avente lo stesso nome, ad esempio la classe (o struct) test_data mostrata in precedenza. Dopodiché, il meccanismo di integrazione consiste semplicemente nella costruzione di una istanza della classe contenente il modello del portable stimulus, e nella successiva invocazione del suo metodo ifc_fill, passando un handle al contenitore test_data del testbench. La Fig. 4 mostra un frammento di codice del testbench C++, contenente la creazione di un handle alla struct test_data – td_h – e di un handle alla classe contenente il modello inFact – td_gen_h. La chiamata al costruttore di quest’ultima definisce anche il nome dell’istanza, che verrà utilizzato internamente da inFact. Tale nome dell’istanza passato a inFact è importante, come verrà illustrato più avanti. Si può notare, all’interno del ciclo for presente nel test C++, la chiamata al metodo ifc_fill, seguita dall’assegnamento dei contenuti dell’istanza della struct td_h a delle corrispondenti variabili locali, che verranno poi passate alla funzione C che costituisce il Dut di questo testbench. Questa architettura non è particolarmente differente dall’utilizzo di un elemento classe o sequence per la randomizzazione in SystemVerilog tramite la chiamata alla .randomize(), oppure dall’uso di una classe in SystemC/Scv unitamente al suo metodo “next”. L’unica differenza consiste nel fatto che il modello che effettua la randomizzazione è in effetti un modello di grafo inFact. Per quanto visto finora, il valore aggiunto portato dall’utilizzo del modello portable stimulus di inFact consiste nella capacità di randomizzare numerosi valori numerici, rispettando al contempo qualsiasi insieme di vincoli algebrici che siano stati imposti su tali valori, o sulle loro reciproche relazioni.
Considerando anche la copertura
Un ulteriore valore apportato dal modello di inFact consiste però nel fatto che esiste anche un altro tipo di input che può essere sovraimposto al modello dello stimolo, un input che viene denominato “strategia di copertura”. Tale strategia può essere considerata come qualcosa di analogo a un covergroup di SystemVerilog, nel senso che definisce le variabili di interesse e i bin desiderati per il conteggio dei loro valori, così come delle combinazioni di tali variabili. La differenza fondamentale risiede nel fatto che questa informazione, fornita come input al processo di randomizzazione, altera anche la distribuzione dei valori randomici generati, in modo tale da coprire in modo efficiente gli obiettivi della strategia. Le metriche di copertura che vengono misurate non sono in questo caso costituite da coverpoint (o cross) di natura funzionale, quanto piuttosto indici di copertura del codice, situazione più comune all’interno degli ambienti C/C++ (sebbene sia possibile implementare anche la copertura di tipo funzionale). Gli obiettivi definiti all’interno della strategia di copertura dovrebbero quindi essere costituiti, come suggerito dal nome, dall’implementazione di una (o più) logiche tese ad ottenere un elevato tasso di copertura del codice, oppure indirizzate verso aree specifiche del codice escluse da altre strategie di verifica. Poiché il Dut considerato in questo esempio - il moltiplicatore - è piuttosto semplice, può essere sufficiente una strategia altrettanto minimale. Tra gli strumenti inclusi all’interno di inFact vi sono alcune utility che consentono la creazione assistita di strategie di copertura sulla base di una varietà di possibili input, ivi incluse strategie automatizzate orientate ai tipi di dati predefiniti, strategie custom definite mediante l’uso di file csv oppure di fogli di calcolo, come anche un editor di tipo grafico. Nell’esempio qui considerato può essere utilizzata una strategia automatizzata che indirizza ogni variabile di stimolo in modo isolato, vale a dire senza utilizzare elementi di tipo cross. Per ogni variabile presente nella gerarchia test_data (incluso ogni elemento dei diversi array), la utility è in grado di derivare l’elenco di tutti i valori consentiti, grazie ad una analisi dei vincoli definiti, e successivamente di ripartirlo su un numero indicato di bin distinti. Per questo esempio è stato specificato l’utilizzo di un totale di 128 bin, in quanto ciò consente di assicurare la copertura di tutti i possibili valori di coeff, per ogni elemento di 7 bit presente nell’array. Poiché è possibile, quando desiderato, aggiungere anche degli edge-bin separati (dedicati ai singoli valori minimo e massimo del range ammissibile), in questo caso per le variabili con i valori più estesi - gli elementi di dati da 10 bit - sono stati creati due bin specifici per i valori estremi dell’intervallo. Come auspicato, l’esecuzione fino al suo completamento della strategia automatizzata ha prodotto ottimi risultati di copertura del codice ottenendo il 100% (mentre i risultati di un test iniziale basato su un approccio di semplice randomizzazione erano del 20% più bassi).
Portable Stimulus e stabilità della randomizzazione
L’ottenimento di un elevato tasso di copertura del codice è senza dubbio un risultato apprezzabile, ma la vera finalità di questo articolo è di descrivere come sia possibile sviluppare un modello di stimolo, unitamente ad una o più strategie di copertura ad esso correlate, nel contesto di un dominio e successivamente rieseguire il tutto all’interno di un dominio differente. L’elemento che rende ciò possibile, vale a dire il seed della randomizzazione operata da un modello di stimolo di inFact, può essere esplicitamente definito dall’utente, oppure semplicemente derivato dall’esecuzione originale, che lo può salvare in un file di output. La versione in SystemVerilog del modello può quindi essere eseguita all’interno di un testbench SV, per sollecitare il Dut in Rtl, seguendo la medesima procedura utilizzata per la versione in C, ovvero semplicemente istanziando l’oggetto della classe SV che contiene il modello, e successivamente invocando il suo task predefinito - ifc_fill - per randomizzare i contenuti della classe SystemVerilog test_data. In questo caso è stato necessario riformattare i packed array utilizzati all’interno della classe test_data, per adattarli alle più estese dimensioni degli oggetti registro che costituiscono gli input del Dut in questo contesto, ma si tratta di un’operazione alquanto semplice, ottenuta mediante l’utilizzo di un operatore di concatenamento – {arrEl[0], ... , arrEl[N]}. In questo esempio si può anche osservare come sia possibile interrogare lo stato corrente della strategia di copertura, tramite un’altra funzione predefinita della strategia - allCoverageGoalsHaveBeenMet() - che può essere utilizzata come qualificatore per definire nuovi input, oppure per definire una condizione di uscita dal ciclo di test. Eseguendo il testbench SystemVerilog si è ottenuta una copertura del codice Rtl del Dut ancora estremamente elevata - 97.11% in questo caso - lasciando girare il codice fino a completa terminazione della strategia di copertura, come illustrato in Fig. 7. Nonostante la sua semplicità, questo esempio illustra il grado di riutilizzabilità reso disponibile dal modello comune di portable stimulus presente nella suite di strumenti Questa inFact. Naturalmente, sarà probabilmente necessario prevedere sempre una serie di test aggiuntivi specifici per la versione Rtl del Dut, allo scopo di gestire gli aspetti addizionali del comportamento inseriti nell’Rtl frutto della sintesi. Ciò perché il processo di Hls crea strutture aggiuntive che non sono presenti nella descrizione C++ non temporizzata, come ad esempio i protocolli di interfacce che prevedono condizioni di stallo, gli automi di controllo a stati finiti, o le logiche di clock e reset. Comunque, ottenendo una chiusura pari al 100% della copertura sul codice C++ utilizzando un modello di portable stimulus di inFact, si può avere la garanzia di raggiungere lo stesso grado di copertura delle funzionalità progettuali nel corso della verifica sull’Rtl. Dopodiché, si tratta solo di aggiungere ulteriori test per coprire le strutture rimanenti, aggiunte dall’attività di Hls.