In questa puntata vedremo in maniera più sistematica e con un maggiore livello di dettaglio alcuni aspetti rilevanti del SYSTEM C tra i quali i concetti di moduli, processi ed eventi; approfondiremo inoltre come opera il kernel di simulazione.
La figura 1 mostra in notazione grafica i diversi elementi previsti in linguaggio SystemC e che saranno descritti nel seguito del tutorial; in figura 2 è mostrato quindi un esempio di come questi elementi concorrano alla descrizione di un sistema.
I componenti fondamentali sono i moduli (sc_module) che comunicano con l’esterno attraverso le porte (sc_port); al loro interno sono contenuti altri moduli o processi ( sc_method, sc_thread, sc_cthread) che descrivono le diverse funzionalità. La comunicazione tra moduli e processi avviene tramite porte, interfacce (sc_interface) e canali (sc_signal); i diversi processi sono sincronizzati sulla base degli eventi (sc_event).
I moduli: come descriverli
Un modulo SystemC, come osservato in precedenza, è l’elemento fondamentale della descrizione hardware in tale linguaggio; è equivalente al concetto di entità in VHDL. Dal punto di vista formale, in linguaggio C++ rappresenta semplicemente la definizione di una classe; può contenere i seguenti elementi:
➤ porte
➤ istanze di canali membro
➤ istanze di dati membro
➤ istanze di moduli membro
➤ costruttore
➤ distruttore
➤ processi
➤ helper function
La sintassi per la definizione di un modulo è la seguente :
SC_MODULE (module_name)
{
// module_body
// constructor
}
oppure
class module_name : public sc_module
{
// module body
//constructor
}
Il costruttore è la funzione membro del modulo che ha principalmente il compito di creare, inizializzare e connettere eventuali oggetti membro e registrare i processi all’interno del kernel di simulazione SystemC; la sintassi fondamentale per la descrizione del costruttore, all’interno della definizione del modulo, è la seguente: SC_CTOR(module_name) :: Initialization // optional
{ Subdesign_Allocation Subdesign_Connectivity Process_Registration Miscellaneous_Setup
}
Un metodo alternativo si basa sulla macro SC_HAS_PROCESS; in questo caso le seguenti istruzioni sono incluse nella definizione del modulo: SC_HAS_PROCESS(module_name); module_name (sc_module_name instname[,others_args]); Il costruttore può quindi essere dichiarato nel modo seguente anche all’esterno dalla definizione del modulo: module_name::module_name (sc_module_name instname[,others_args]) : sc_module (instname) [,others_initializers]
{
// Subdesign_Allocation
// Subdesign_Connectivity
// Process_Registration
// Miscellaneous_Setup
}
Riconsideriamo ad esempio la descrizione del contatore discussa nella prima parte del tutorial; di seguito è riportata la descrizione del costruttore che avevamo utilizzato:
SC_CTOR (cnt)
{
SC_METHOD (cnt_comb);
sensitive << enable << value_r; SC_METHOD (cnt_reg); sensitive_pos << reset << clk;
}
Sono registrati due processi cnt_comb e cnt_reg che descrivono rispettivamente la logica combinatoria e registrata del contatore. In alternativa avremmo potuto utilizzare la seguente sintassi:
SC_MODULE(cnt)
{
… SC_HAS_PROCESS(cnt);
cnt (sc_module_name cntinst);
…
}
cnt::cnt (sc_module_name cntinst)
: sc_module (cntinst)
{
SC_METHOD (cnt_comb);
sensitive << enable << value_r; SC_METHOD (cnt_reg); sensitive_pos << reset; sensitive_pos << clk;
}
L’utilizzo del costrutto SC_HAS_PROCESS, oltre a consentire la descrizione del costruttore all’esterno della classe modulo (aspetto che può risultare di interesse nei casi in cui si vogliano creare librerie da distribuire non in formato sorgente per proteggere i dettagli implementativi e strutturali dei diversi moduli) permette inoltre di dichiarare alcuni parametri generici che vengono specificati al momento della elaborazione del modulo. Nel caso precedente, ad esempio, il codice può essere modificato per supportare la configurazione dell’istanza del contatore come di tipo up o down; il principio è del tutto equivalente al concetto di generic in VHDL. Nel listato 1 sono riportate le modifiche da apportare.
enum cnttype {up, down}; SC_MODULE(cnt) { … void cnt_comb_up(); void cnt_comb_down(); … SC_HAS_PROCESS(cnt); cnt (sc_module_name cntinst, cnttype type); … } cnt::cnt (sc_module_name cntinst, cnttype type) : sc_module (cntinst) { switch(type) { case up: SC_METHOD (cnt_comb_up); sensitive << enable << value_r; break; case down: SC_METHOD (cnt_comb_down); sensitive << enable << value_r; break; } … } void cnt::cnt_comb_up() { … value_c = value_r.read() + 1; … } void cnt::cnt_comb_down() { … value_c = value_r.read() - 1; … }
Listato 1 |
La possibilità, in effetti, di parametrizzare un modulo è una delle caratteristiche più interessanti del SystemC; oltre allo stile discusso e che si riferisce direttamente alla fase di elaborazione, sono previsti costrutti che consentono la definizione dei parametri all’atto della compilazione o della simulazione. La discussione dettagliata di questi aspetti esula gli scopi del presente tutorial che ha carattere prevalentemente introduttivo al linguaggio; il lettore interessato può fare riferimento a [5] dove è riportata una descrizione approfondita di questi aspetti.
I moduli: come connetterli
I moduli membro comunicano, come avremo modo di vedere in seguito, principalmente mediante porte connessi a canali; esistono diversi metodi per descrivere una istanza di un modulo e definire le sue connessioni. Una soluzione, già vista nella precedente puntata, consiste nella definizione di un oggetto del tipo del modulo da istanziare; all’inter no del costruttore del modulo contenitore (o della funzione sc_main) le connessione sono descritte mediante istruzioni del tipo:
module_name_inst.(port_name) (channel_name);
In alternative può essere definito un puntatore ad un oggetto del tipo del modulo da istanziare; il puntatore viene creato nel costruttore dell’oggetto che lo contiene mediante l’operatore new e le connessioni sono definite mediante l’operatore -> utilizzando la seguente sintassi: module_name *module_name_inst = new module_constructor;
module_name_inst -> port_name (channel_name);
In seguito avremo modo di vedere in dettaglio maggiore i concetti di porte e canali; per ora è sufficiente pensare in maniera intuitiva alle porte come le interfacce di comunicazione con l’esterno del modulo e considerare come caso particolare di un canale i segnali. Rivediamo quindi il codice che avevamo scritto per instanziare il contatore all’interno del test-bench nell’esempio discusso nella prima parte del tutorial:
cnt dut;
dut.reset(reset);
…
dut.value_rco(cnt_rco);
Utilizzando i puntatori, avremmo potuto in
maniera diversa descrivere l’istanza del modulo come segue:
cnt *dut;
dut = new cnt(“4bit_Counter”);
dut->reset(reset);
…
dut->value_rco(cnt_rco);
…
delete dut;
L’utilizzo di puntatori, in particolare, ha il chiaro di vantaggio di consentire una allocazione dinamica della memoria mediante gli operatori new e delete.
Processi ed eventi
Abbiamo già incontrato, negli esempi discussi finora, il concetto di processo. In generale, è una funzione membro od un metodo della classe SC_MODULE; il prototipo per la sua definizione è il seguente: void process_name (); I processi vengono registrati all’interno del kernel di simulazione SystemC dal costruttore del modulo mediante una delle seguenti istruzioni:
SC_METHOD (method_name); SC_THREAD (thread_name);
SC_CTREAD (cthread_name,
clock_name.egde()); A seconda della modalità nella quale il processo viene registrato, cambiano le sue modalità di esecuzione. Un processo di tipo SC_THREAD, ad esempio, può essere eseguito una sola volta; può terminare rilasciando il controllo al kernel di simulazione oppure essere sospeso temporaneamente mediate la funzione membro wait() specificata in seguito. Al contrario, SC_METHOD sono processi che non possono essere sospesi ma possono essere richiamati più volte dal kernel. Si noti ad esempio, come, nel caso del contatore discusso nella prima parte del tutorial, i metodi sono stati utilizzati per descrivere la logica del circuito,mentre i thread per dichiarare la sequenza di segnali di controllo nel test-bench. I processi SC_CHTREAD, infine, sono particolari tipi di thread attivati in corrispondenza del fronte del segnale clock_name specificato nella dichiarazione; supportano il concetto di watched signals, ovvero la possibilità di specificare, in funzione dell’evento che riattiva il processo, un salto del punto di ripristino rispetto all’ultima istruzione sequenziale eseguita prima della sospensione; maggiori dettagli sono discussi in [4]. Le condizioni che attivano in generale un processo all’interno del kernel di simulazione sono le seguenti:
➤ attivazione di una istanza durante la fase di inizializzazione;
➤ occorrenza di un evento rispetto al quale il processo è sensibile;
➤ occorrenza di un time-out;
➤ chiamata alla funzione sc_spawn durante la fase di simulazione (nel caso di processi dinamici).
Un evento è un oggetto della classe sc_event; le funzioni membro notify(time) e cancel() consentono rispettivamente di schedulare un evento all’istante di tempo time o cancellarlo. Consideriamo ad esempio quanto riportato nel listato 2.
#include “systemc.h” SC_MODULE(print_message) { sc_event print; char *message; void print_my_message() { cout << “Time :” << sc_time_stamp() << “\t” <<this->message << endl; }; SC_CTOR(print_message) { SC_THREAD(print_my_message); sensitive << print; dont_initialize(); } }; int sc_main(int argc, char *argv[]) { sc_set_time_resolution(1, SC_NS); print_message *printer = new print_message(“print_my_message”); printer->print.notify(10, SC_NS); printer-> message = “Hello word - SystemC”; sc_start(20, SC_NS); return 0; }
Listato 2 |
Il risultato della esecuzione del programma è il print su schermo del messaggio “Hello word SystemC” all’istante di tempo 10 nanosecondi (relativamente all’inizio della simulazione). Si noti l’utilizzo dell’istruzione dont_initialize() che evita l’esecuzione del thread all’atto dell’inizializzazione del kernel (come invece previsto dallo standard nei casi normali). Omettendo tale istruzione il processo sarebbe stato eseguito all’istante iniziale; la successiva occorrenza dell’evento non avrebbe avuto alcun effetto trattandosi di un thread non riattivabile. Un altro aspetto da chiarire è il seguente. Nell’esempio discusso nella prima parte del tutorial ed altre volte richiamato in precedenza, l’esecuzione dei processi era attivata in corrispondenza della variazione del valore di un segnale. Come avremo modo di discutere in dettaglio nel prosieguo, in effetti, i segnali sono dei tipi particolare di channel del tipo evaluate-update; in corrispondenza della variazione del valore di uno di questi, il kernel di simulazione notifica automaticamente un evento associato che attiva il processo. L’insieme di eventi che, in generale, possono attivare l’esecuzione di un processo (o riprenderla a seguito di una sospensione nel caso di thread) è chiamata sensitivity del processo; si distingue tra:
{1} sensitivity statica: definita durante la fase di elaborazione
{2} dinamica: modificabile durante l’esecuzione sotto il controllo del processo stesso. La sintassi per la dichiarazione di una sensitivity di tipo statico è la seguente:
sensitive << event_1 << event_2 … <<
event_n;
da includere immediatamente dopo l’istruzione di registrazione del processo all’inter no del costruttore. Si veda ad esempio nel caso della descrizione del contatore richiamata inizialmente (listato riga), la definizione del processo combinatorio e della sua sensibilità al valore di conteggio ed al segnale di abilitazione. Nel caso invece di sensitivity sono utilizzate le funzioni membro wait() e next_trigger() – nel caso rispettivamente di thread e metodi – per ridefinire ad ogni passo la lista di eventi cui il processo deve risultare sensibile. I prototipi delle due funzioni sono riportati nel listato 3.
next_trigger (time); // rieseguito dopo time next_trigger (event); // rieseguito all’occorrenza di event next_trigger (event1 |…| eventn); // rieseguito all’occorrenza di uno qualsiasi degli eventi della lista next_trigger (event1 &…& eventn); // rieseguito all’occorrenza di tutti gli eventi // simultaneamente next_trigger (timeout, event); // rieseguito all’occorrenza di event od al termine di timeout next_trigger (timeout, event1 |…| eventn);// rieseguito all’occorrenza di uno qualsiasi degli eventi // della lista od al termine di timeout next_trigger (timeout, event1 &…& eventn);// rieseguito all’occorrenza di tutti gli eventi // simultaneamente od al termine di timeout next_trigger (); // rieseguito all’occorrenza di uno qualsiasi degli eventi // della sensitivity wait (time); // riattivato dopo time wait (event); // riattivato all’occorrenza di event wait (event1 |…| eventn); // riattivato all’occorrenza di uno qualsiasi degli eventi // della lista wait (event1 &…& eventn); // riattivato all’occorrenza di tutti gli eventi // simultaneamente wait (timeout, event); // riattivato all’occorrenza di event od al termine di timeout wait (timeout, event1 |…| eventm); // rieseguito all’occorrenza di uno qualsiasi degli eventi // della lista od al termine di timeout wait (timeout, event1 &…& eventm); // riattivato all’occorrenza di tutti gli eventi // simultaneamente od al termine di timeout wait (); // riattivato all’occorrenza di uno qualsiasi // degli eventi della sensitivity
Listato 3 |
Un metodo può contenere diverse chiamate a next_trigger; qualora questo accada, ognuna sovrascrive la precedente. Prima di chiudere la nostra discussione sui processi, è opportuno segnalare per completezza che, a partire dalla versione 2.1, è stato introdotto in SystemC il concetto di processi dinamici che possono essere inizializzati e distrutti in maniera dinamica in modo da ridurre l’occupazione di risorse nel caso di descrizione di sistemi complessi; una della applicazioni più interessanti è ad esempio quella della configurazione dinamica dei test-bench. Il lettore interessato può trovare una breve introduzione in [4].
Conclusioni
Moduli e processi sono due degli elementi fondamentali di una descrizione in linguaggio SystemC. In questa seconda parte del tutorial abbiamo visto come dichiararli ed utilizzarli. I moduli, in particolare, sono a loro volta connessi da porte e canali accessibili mediante interfacce; nella prossima puntata vedremo di cosa si tratta e come utilizzare questi oggetti.