Il mercato dei dispositivi integrati è ormai da alcuni anni fortemente condizionato dal costante tentativo di ridurre il time-to-market, unica soluzione che consenta di contenere i costi di sviluppo, incrementare i profitti, mantenere competitività. D’altro canto, i continui progressi tecnologici, ancora oggi in accordo alla legge di Moore al di là di ogni ragionevole aspettativa, rendono possibile l’integrazione della maggior parte delle funzionalità di sistema in singoli dispositivi riducendo costi e consumi.
La crescente complessità che ne deriva, tuttavia, comporta tempi di sviluppo e verifica maggiori, in contrasto con l’esigenza sopra esposta di ridurre il time-to-market. Per ricucire quello che è ormai, a tutti gli effetti, un vero e proprio gap di produttività (inteso come divario tra le risorse rese disponibili dalla tecnologia e la reale capacità di sfruttarle) appare necessario condividere e riutilizzare in più progetti comuni componenti hardware/software e realizzare la maggior parte delle funzionalità specifiche mediante applicativi software embedded: in una parola, adottare una metodologia di progetto platform-based. Tale metodologia richiede tuttavia un linguaggio descrittivo che supporti indistintamente modelli hardware e software e consenta un livello di astrazione maggiore; in fondo, l’esigenza non è molto diversa dalla necessità che spinse negli anni ’90 a sostituire i classici metodi di progettazione elettronica basata su porte logiche e schemi elettrici con le rappresentazioni comportamentali ed RTL rese possibili con i linguaggi HDL. In questo ambito, una delle soluzioni più interessanti che si sta affermando nel settore è il SystemC, una libreria di classi che introduce in ambiente C++ i concetti tipici della descrizione hardware quali concorrenza, eventi temporizzati, segnali, moduli, porte. Lo standard SystemC è stato introdotto nel 1999 dall’OSCI (Open SystemC Initiative), organizzazione noprofit fondata per questo scopo da alcune delle principali compagnie del settore elettronico, tra le quali ARM, Cadence, Intel, STMicroelectronics, Synopsys, Mentor. Disponibile in versione open-source è stato recentemente approvato anche dall’IEEE. Tra i vantaggi principali del nuovo linguaggio vi sono:
➤ la disponibilità di diversi livelli di astrazione ai quali descrivere il sistema, da semplici specifiche eseguibili fino ad una rappresentazione cycle-accurate e RTL sintetizzabile;
➤ la realizzazione di un ambiente comune che supporti componenti hardware e software nelle fasi di design-partitionig e co-simulazione o consenta il debug del software su modelli funzionali della architettura hardware prima che i prototipi siano disponibili;
➤ il supporto per un ampio insieme di tipi di dati che includa rappresentazioni a precisione fissa o arbitraria, in particolare nel caso di applicazioni DSP;
➤ la definizione di semantiche per protocolli di comunicazione a diversi livelli ed il supporto di differenti modelli computazionali (reti di processi di Kahn, dataflow multi-rate statico o dinamico, processi sequenziali comunicanti…);
➤ la standardizzazione di un sottoinsieme del linguaggio che sia direttamente sintetizzabile.
A partire da questo numero presenteremo un tutorial sulla libreria SystemC cercando di evidenziarne appunto tali vantaggi e mostrarne le potenzialità; saranno discussi le basi del linguaggio in modo da fornire gli strumenti necessari per iniziare a programmare nel nuovo ambiente.
Un esempio: un contatore a 4-bit
Ogni componente, secondo la più classica rappresentazione, si può immaginare come rappresentato da un simbolo che definisce le porte di interfaccia e da uno schema architetturale sottostante che descrive come i segnali di ingresso sono elaborati per generare le uscite; in VHDL, ad esempio, si utilizzano per descrivere i due aspetti i concetti di entity ed architecture. In SystemC, si utilizza invece il costrutto SC_MODULE per dichiarare un componente; come avremo modo di approfondire in seguito, un oggetto di tipo SC_MODULE è in realtà una classe che include al suo interno una varietà di elementi tra cui porte, processi, istanze di altri moduli membro. Il seguente listato 1 mostra in particolare la dichiarazione del contatore a 4 bit considerato nel nostro esempio di riferimento.
//file cnt.h #include “systemc.h” SC_MODULE (cnt) { sc_in<bool> enable; //enable sc_in<bool> reset; //reset sc_in<bool> clk; // clock input sc_signal<sc_uint<4> > value_c; // next_value sc_out<sc_uint<4> > value_r; // 4-bit counter output sc_out<bool> value_rco; // ripple carry out void cnt_comb(); void cnt_reg(); SC_CTOR (cnt) { //constructor SC_METHOD (cnt_comb); sensitive << enable << value_r; SC_METHOD (cnt_reg); sensitive << reset; sensitive << clk; } };
Listato 1 |
La classe contiene la dichiarazione delle porte di ingresso ed uscita, di eventuali segnali interni e dei due processi, cnt_comb() e cnt_reg(), che serviranno a descrivere la parte combinatoria e registrata del circuito. Quindi viene definito il costruttore della classe SC_CTOR il cui scopo è inizializzare ed instanziare eventuali moduli sottostanti, definire le connessioni tra questi, registrare i processi all’interno del kernel di simulazione SystemC; il costruttore definisce ovvero l’architettura del modulo. Nell’esempio di riferimento, in particolare, sono stanziati i due processi definiti in precedenza; il file cnt.cpp il cui contenuto è riportato nel listato 2 contiene la loro descrizione.
// file cnt.cpp #include “cnt.h” void cnt::cnt_comb() { if (enable.read()) value_c = value_r.read() + 1; if (value_r.read()==0xF) value_rco=1; else value_rco=0; } void cnt::cnt_reg() { if (reset.read()) value_r.write(0x0); else value_r.write(value_c); }
Listato 2 |
In calce alla stanziazione nel costruttore, per ognuno dei processi definiti, l’istruzione sensitive (o sensitive_pos) riportata la lista dei segnali la cui variazione attiva l’esecuzione del processo, analogamente alla dichiarazione della sensitivity list in linguaggio VHDL. Cnt_Reg, ad esempio, che descrive la logica registrata è correttamente configurato come sensibile al fronte positivo del clock ed al livello del segnale di reset; quando questo è asserito, il processo inizializza il valore del contatore, in caso contrario registra il prossimo valore in corrispondenza della transizione del segnale di clock. Il prossimo valore è calcolato da Cnt_Comb che incrementa il conteggio qualora sia abilitato; genera inoltre in maniera combinatoria il segnale di riporto. In entrambi i casi, per accedere alle porte del modulo dall’interno dei processi sono stati utilizzati i metodi .read e .write; più avanti questo tipo di costrutto sarà chiarito in maggiore dettaglio.
Il test-bench di simulazione
Il listato 3 riporta un semplice test-bench di simulazione che può essere utilizzato per verificare il corretto funzionamento del contatore descritto in precedenza.
// file test_bench.cpp #include “cnt.h” #include “driver.h” #include “monitor.h” int sc_main(int argc, char *argv[]) { sc_signal<bool> reset; // asynchronous reset sc_clock sysclk(“sysclk”, 10, SC_NS); // 100 MHz system clock sc_signal<bool> cnt_enable; // clock enable sc_signal<sc_uint<4> > cnt_value; // counter output sc_signal<bool> cnt_rco; // counter output // instantiate device under test and connect ports cnt dut(“4bit_Counter”); dut.reset(reset); dut.clk(sysclk); dut.enable(cnt_enable); dut.value_r(cnt_value); dut.value_rco(cnt_rco); // instantiate driver and connect ports driver stimuli(“GenerateStimuli”); stimuli.clk(sysclk); stimuli.reset(reset); stimuli.enable(cnt_enable); // instantiate monitor and connect ports monitor monit(“LogValue”); monit.value(cnt_value); // create VCD file and log signals sc_trace_file *VCDLogFile = sc_create_vcd_trace_file (“VCDLogFile”); sc_trace(VCDLogFile, sysclk, “sysclk”); sc_trace(VCDLogFile, reset, “reset”); sc_trace(VCDLogFile, cnt_enable, “enable”); sc_trace(VCDLogFile, cnt_value, “cnt_value”); // start simulation sc_start(10000, SC_NS); // close VCD LogFile sc_close_vcd_trace_file(VCDLogFile); // return value return(0); }
Listato 3 |
Il test-bench include una istanza del componente da verificare ed i moduli driver e monitor che hanno come scopo la generazione dei segnali di ingresso ed il log dei segnali di uscita. Ognuna di queste istanze è creata tramite la definizione di un oggetto del tipo corrispondente; ad esempio dut è un oggetto di tipo cnt. Per ogni oggetto, vengono poi dichiarate le connessioni delle porte ai segnali interni; ad esempio l’istruzione dut.clk(sysclk) connette l’ingresso clk del contatore da verificare al segnale di clock. Porte di oggetti diversi connessi allo stesso segnale sono evidentemente in comunicazione; in questi termini la semantica del linguaggio SystemC è del tutto equivalente alle descrizioni strutturali in VHDL. Una particolare istruzione serve a definire il segnale di clock come un oggetto di tip sc_clk; è possibile specificarne, tra l’altro, periodo e duty-cycle. L’istruzione sc_start definisce, invece, il tempo di simulazione. Mediante le istruzioni sc_trace_file ed sc_trace, infine, viene creato un file in formato VCD dove sono indicati i segnali di cui fare il log; l’istruzione sc_close chiude il file al termine della simulazione. I file VCD possono essere visualizzati come forme d’onda mediante appositi viewer; tra questi vi è ad esempio GTKWave che è disponibile in versione free all’indirizzo web. Il listato 4 riporta la descrizione dei moduli driver e monitor.
//file driver.h #include “systemc.h” SC_MODULE(driver) { sc_in<bool> clk; sc_out<bool> enable; sc_out<bool> reset; void prc_driver(); SC_CTOR (driver) { SC_THREAD(prc_driver); } }; //file driver.cpp #include “driver.h” void driver::prc_driver() { reset = 1; enable = 0; wait(100,SC_NS); reset = 0; wait(100, SC_NS); wait(3, SC_NS); enable = 1; wait(100, SC_NS); wait(1, SC_NS); enable = 0; wait(100, SC_NS); reset = 1; }; //file monitor.h #include “systemc.h” SC_MODULE(monitor) { sc_in<sc_uint<4> > value; ofstream outfile; void prc_monitor(); SC_CTOR (monitor) { SC_METHOD(prc_monitor); sensitive << value; outfile.open(“LogFile.log”); } }; //file monitor.cpp #include “monitor.h” void monitor::prc_monitor() { // text based debug outfile << “Time :” << sc_time_stamp() << “\tValue : “ << value << endl; };
Listato 4 |
Il monitor riporta in un file di testo il valore dell’uscita del contatore nel momento in cui cambia; accanto al valore viene anche scritto il tempo dell’evento, restituito dal kernel di simulazione mediante l’istruzione sc_time_stamp(). Il driver, invece, definisce la sequenza temporale dei segnali di reset ed enable del contatore; utilizza, diversamente dai moduli visti in precedenza, un’istruzione di tipo SC_THREAD all’interno del costruttore. Tale istruzione definisce un processo che viene iniziato all’atto della registrazione nel kernel SystemC ma può essere arrestato in qualunque istante mediante una istruzione di tipo wait; tuttavia non può essere ripetuto al termine della sua esecuzione. I processi SC_METHOD, invece, vengono eseguiti soltanto quando si verifica un evento in uno dei segnali della sua sensitivity list, non possono essere arrestati, terminano quando si esaurisce la lista di istruzioni incluse ma possono essere ripetuti più volte.