SystemC – Interfacce, porte e canali

Il linguaggio SystemC supporta in maniera nativa una descrizione delle architetture di tipo gerarchico basata sul concetto di modulo. In tutte le applicazioni, tale approccio semplifica la comprensibilità del progetto, facilita il design partitioning, consente il riutilizzo  di componenti, riduce i tempi verifica; richiede tuttavia la definizione di oggetti per la connessione dei diversi moduli ed attraverso cui comunicare  i dati. In linguaggio SystemC sono introdotti per questo i concetti di interfaccia, canale e porta; si basano sulle idee di classi virtuali e polimorfismo che rappresentano alcune delle più interessanti caratteristiche del linguaggio C++.

Definizioni e concetti introduttivi

In SystemC un’interfaccia è una classe astratta che eredita dalla classe sc_interface e fornisce dichiarazioni di funzioni virtuali e metodi; in altri termini dichiara quali operazioni possono essere eseguite per gestire il trasferimento dei dati. Un canale è quindi una classe che implementa una o più interfacce ed eredita dalle classi sc_channel (canali gerarchici) o sc_prim_channel (canali primitivi); è necessario supporti tutti i metodi previsti dalla interfaccia acquisita. Un canale, in altri termini,  realizza i modi di trasferimento dei dati previsti dalle interfacce supportate. I canali  di tipo gerarchico sono in realtà moduli; possono quindi contenere processi e moduli componenti. Sono tipicamente utilizzati per la descrizione di protocolli di comunicazione complessi come PCI o AMBA o per creare adaptors/transactors, come avremo modo di vedere in seguito. Attraverso la definizione delle interfacce a livello astratto e dei canali come implementazioni di queste, il SystemC fornisce uno schema che consente di definire la struttura delle connessioni dei moduli indipendentemente dalla realizzazione di queste; diverse realizzazione potranno quindi essere descritte a diversi livelli di astrazione. Una porta, infine, è una classe templated e che eredita da una interfaccia; consente l’accesso a canali esterni al modulo. La sintassi per la definizione di una porta, come visto negli esempi utilizzati nelle precedenti puntate, è del tipo:

SC_MODULE(module_name)
{ …
sc_port<interface_name> port_name;

}

Vediamo, un esempio che possa chiarire i concetti presentati: la realizzazione di un semplice modello di canale che descriva una memoria FIFO (First-In First-Out); l’esempio è adattato da quello incluso nella distribuzione del linguaggio SystemC che è possibile scaricare dal sito [1]. Il tipo di dati supportato dalla FIFO è ereditato mediante templating; la dimensione della FIFO può essere definita all’atto della chiamata del costruttore.

Un semplice esempio

Il primo passo nella costruzione del modello è la dichiarazione delle interfacce supportate dalla memoria, ovvero le operazioni di scrittura e lettura; il codice corrispondente è riportato nel listato 1.

template <class T> class fifo_write_if : virtual public sc_interface
{
      public:
      virtual void write(const T&) = 0;
      virtual void reset() = 0;
};
template <class T> class fifo_read_if : virtual public sc_interface
{
     public:
     virtual void read(T&) = 0;
     virtual int num_available() = 0;
};
Listato 1

Quindi viene creato il canale FIFO fornendo una definizione di queste interfacce, come mostrato nel listato 2.

template <class T> class fifo : public sc_channel, public
fifo_write_if<T>, public fifo_read_if<T>
{
   public:
fifo(sc_module_name name, unsigned size) : sc_prim_channel(name),
_size(size)
{
        assert (size > 0);
        _first = _items = 0;
        _data = new T[_size];
}
void write(const T& _value) {
        if (_items == _size)
        wait(read_event);
        _data[(_first + _items) % _size] = _value;
        ++ _items;
        write_event.notify();
}
void read(T& _value){
        if (_items == 0)
        wait(write_event);
        _value = _data[_first];
        _items;
        _first = (_first + 1) % _size;
        read_event.notify();
}
void reset() { _items = _first = 0; }
        int num_available() { return _items;}
        private:
        T* _data;
        unsigned _items, _first, _size;
        sc_event write_event, read_event;
};
Listato 2

Oltre alla implementazione  delle interfacce sono dichiarati ed utilizzati degli oggetti che servono, ad esempio, a definire il puntatore alla prossima locazione in lettura (_first) od il numero di elementi contenuti nella FIFO (_items); questi sono evidentemente contenuti in un’area privata in modo da precluderne l’accesso dall’esterno della classe. Il modello della FIFO che stiamo considerando è di tipo blocking. In altri termini, se, ad esempio, la FIFO è vuota, un eventuale accesso in lettura viene sospeso fino alla prossima scrittura; si noti infatti la presenza dell’istruzione wait(write_event) nella descrizione del metodo read(). Allo stesso modo, un accesso in scrittura viene bloccato in condizioni di FIFO piena; in questo caso il processo viene arrestato in attesa di un evento di tipo read_event.  Il listato 3 presenta un esempio di utilizzo della FIFO; viene instanziata una FIFO di 32 locazioni contenenti oggetti di tipo char.

// Producer Module
class producer : public sc_module
{
  public:
  sc_port<fifo_write_if<char> > out;
  SC_HAS_PROCESS(producer);
  producer(sc_module_name name) : sc_module(name)
  {SC_THREAD(main); }
void main()
{
  const char *str =
  “Visit www.systemc.org and see what SystemC can do for you today!\n”;
  while (*str) out->write(*str++);
  }
};

class consumer : public sc_module
{
  public:
  sc_port<fifo_read_if<char> > in;
  SC_HAS_PROCESS(consumer);
  consumer(sc_module_name name) : sc_module(name)
  {SC_THREAD(main); }
void main()
{
  char c;
  cout << endl << endl;
  while (true) {
  in->read(c);
  cout << c << flush;
  }
  }
};
class top : public sc_module
{
  public:
  fifo<char> fifo_inst;
  producer prod_inst;
  consumer cons_inst;
  top(sc_module_name name) : sc_module(name), fifo_inst(“Fifo”, 32),
prod_inst(“Producer”), cons_inst(“Consumer”)
{
  prod_inst.out(*fifo_inst);
  cons_inst.in(*fifo_inst);
}
};

// SC_MAIN
int sc_main (int argc , char *argv[]) {
  top top1(“Top1”);
  sc_start();
  return 0;
}
Listato 3

In figura 1 è mostrata schematicamente l’architettura descritta utilizzando la notazione grafica prevista dal linguaggio SystemC ed introdotta nella precedente puntata.

Figura 1: un esempio di connessioni di moduli mediante canali.

Figura 1: un esempio di connessioni di moduli mediante canali.

Come si vede, il modulo top include una instanza di un componente di tipo producer connesso ad un canale fifo_inst di tipo FIFO mediante una porta di tipo fifo_write_if; un modulo consumer accede invece alla porta di lettura della memoria mediante il metodo read() ereditato dall’interfaccia fifo_read_if del canale. Nel test-bench di simulazione, il producer utilizza la FIFO per passare al consumer una stringa di caratteri che viene scritta sullo schermo.

Interfacce standard e canali base

Il SystemC definisce alcune interfacce e canali standard - che ereditano dalla classe base sc_prim_channel - che possono essere utilizzati nella maggior parte delle applicazioni for nendo alcuni dei più diffusi metodi di connessione dei moduli. sc_fifo_in_if<> ed sc_fifo_out_if<>, un esempio, sono le interfacce implementati dal canale di tipo sc_fifo che realizza il modello di una memoria FIFO del tipo descritto in precedenza. L’interfaccia sc_fifo_out, in particolare, include i  metodi write() ed nb_write() per la scrittura in memoria e num_free() che ritorna il numero di locazioni libere; la funzione data_read_event(), invece, ritorna un riferimento ad un evento che segnala un accesso in lettura alla FIFO e che può essere utilizzato per interrompere dinamicamente l’esecuzione dei threads. I metodi di accesso in lettura ad un canale sc_fifo sono dichiarati nell’interfaccia sc_fifo_in . Read() ed nb_read() consentono la lettura dalla memoria, num_available() ritorna il numero di locazioni occupate, data_written_event() attende un evento di scrittura. Come nel l’esempio discusso in precedenza,  il canale sc_fifo<> implementa le interfacce in maniera blocking; per questo i  metodi implementati possono essere utilizzati soltanto all’interno di sc_threads (i processi la cui esecuzione può essere sospesa). La sintassi per la definizione di un oggetto di tipo sc_fifo è la seguente: sc_fifo<element_typename>sc_fifo_name(size) dove element_typename  definisce il tipo di oggetti da memorizzare nelle locazioni e size il numero di queste. Ad esempio sc_fifo<sc_int<8> > mybuffer(32); definisce mybuffer come una FIFO di interi ad 8 bit, di profondità pari a 32 locazioni. Sc_mutex_if<> è l’interfaccia implementata dal canale sc_mutex<> per descrivere un oggetto mutuamente esclusivo, ovvero un oggetto che definisce una risorsa condivisa - come ad esempio un file - cui possono accedere diversi threads del programma senza incorrere in errori di collisione; anche in questo caso, i metodi implementati dal canale sono di tipo blocking e quindi utilizzabili solo in sc_threads. Lock() consente di riservare la risorsa al processo che la richiede; unlock() permette di rilasciarla, mentre trylock() ritorna lo stato della risorsa. La sintassi per la dichiarazione di un oggetto di tipo sc_mutex è la seguente: sc_mutex mutex_name; Analogamente ad sc_mutex<>, sc_semaphore<> descrive una risorsa condivisa di cui possono esistere più di una copia; si pensi ad esempio ad un area di parcheggio caratterizzata dal numero di piazzole di sosta disponibili. In questi termini, un sc_mutex<> può essere considerato come un sc_semaphore<> con una sola unità. La sintassi per la dichiarazione si un sc_semaphore è la seguente: sc_semaphore semaphore_name(semaphore count); Come nel caso del canale sc_fifo<>, infine, sc_signal_in_if<>, sc_signal_out_if<> ed sc_signal_inout_if<> sono le interfacce implementate dal canale sc_signal<> per le operazioni di accesso in ingresso, uscita e ingresso/uscita.  Il concetto di sc_signal<> è del tutto equivalente a quello di segnale in VHDL. Write() e read() sono i metodi per accedere al canale in scrittura e lettura rispettivamente; write() in particolare include la chiamata alla funzione request_update() del kernel di simulazione che implementa  i meccanismi di evaluate_update su cui si basa il SystemC, come visto nel riquadro di approfondimento nella precedente puntata; tale meccanismo rende del resto possibile la definizione di un metodo event() che riporta se il  canale è stato soggetto ad evento nel precedente ciclo di simulazione. Nella prossima puntata approfondiremo il concetto di canali di tipo evaluate-update. La sintassi per la dichiarazione di un segnale è la seguente: sc_signal<data_type> signal_name; dove data_type  definisce il tipo tra quelli supportati dal SystemC e signal_name il nome del canale.

I tipi di dati in SystemC

Il SystemC  supporta tutti i tipi di dati nativi previsti dal linguaggio C++ essendo una libreria di classi in questo ambiente. Sono inoltre definiti tipi addizionali necessari per rappresentare le architetture hardware; questi tipi sono riferiti all’interno dello standard come SystemC data types:

➤ interi a precisione limitata, derivati dal

le classi sc_int_base o sc_uint_base per la rappresentazione a precisione limitata;

➤ in funzione della lunghezza di parola uti-

lizzata e del tipo C++ nativo sottostante - di interi signed ed unsigned;

➤ interi a precisione limitata, derivati dalle classi sc_signed o sc_unsigned per la rappresentazione a precisione finita limitata dalla lunghezza di parola utilizzata;

➤ interi a virgola fissa, derivati dalle clas-

si sc_fxnum, per la rappresentazione di dati signed od unsigned con precisione limitata dalla lunghezza di parola utilizzata e dai modi di quantizzazione ed overflow impostati;

➤ interi a virgola fissa con precisione variabile modificabile a run-time e non soggetti a quantizzazione ed overflow, derivati dalla classe sc_fxval_fast;

➤ tipi logici singolo-bit, derivati dalla classe sc_logic per la rappresentazione dei valori logic_0, logic_1, high_impedance, high_impedance;

➤  vettori  di  bit  derivati  dalla  classe sc_bv_base che consente di rappresentare i valori logic_0, logic_1;

➤  vettori  logici  derivati  dalla  classe sc_lv_base che consente di rappresentare vettori di bit logici.

Formalmente, ad eccezione dei tipi logici singolo-bit, dei tipi a virgola fissa a precisione variabile e variabile limitata, sono definite classi base con metodi comuni e quindi derivati classi templated con queste che consentono alle applicazioni di specificare la lunghezza di parola da utilizzare come argomento templated. La tabella 1 riassume i diversi tipi del SystemC indicandone le classi di rappresentazione base e templated.

TABELLA 1 - I tipi di dati SystemC.

TABELLA 1 - I tipi di dati SystemC.

Conclusioni

Interfacce, canali e porte sono i metodi utilizzati per la connessione dei moduli e la comunicazione dei dati. Il SystemC  definisce le strutture base che consentono di creare in maniera astratta lo schema di connessioni e definirne  i dettagli implementativi a diversi livelli di astrazione. In questa puntata abbiamo introdotto i concetti base; nel prossimo numero approfondiremo l’argomento presentando  i canali di tipo evaluate-update e vedremo come creare canali custom, in particolare di tipo gerarchico.

 


 

Scrivi un commento