- Elettronica Open Source - https://it.emcelettronica.com -

System C – Canali Custom

Interfacce, canali e porte sono i metodi in SystemC per la connessione dei moduli. Nella precedente puntata [1] abbiamo presentato una introduzione a questi concetti. In questo numero approfondiremo i canali primitivi di tipo evaluate-update e vedremo come creare canali gerarchici.

Uno degli aspetti più complessi della modellizzazione hardware è la descrizione della concorrenza degli eventi. L’esempio più classico è la descrizione di un registro a scorrimento. Ogni elemento del registro può essere descritto con un processo; tuttavia il kernel  di simulazione SystemC, operando in maniera sequenziale, non assicura l’ordine temporale con cui questi vengono eseguiti. Quindi se il  processo associato ad un registro n viene eseguito prima di quello associato al registro n+1, questo assume il valore errato. La controparte software di questo aspetto è, come noto, la necessità di utilizzare una variabile temporanea per eseguire lo scambio di due altre variabili. Nella precedente puntata abbiamo visto che il motore di simulazione SystemC basa la corretta descrizione della concorrenza su un modello di simulazione di tipo evaluate-update.  I canali primitivi prevedono uno schema specifico di chiamate a metodi per il supporto di questo paradigma; il concetto è del tutto equivalente a quello di delta-cycle in VHDL [2]. Per capire come funziona, vediamo la semplice realizzazione di un canale di tipo sc_signal (alternativa a quella inclusa in maniera nativa nel linguaggio). Il listato 1 mostra la definizione delle interfacce sc_signal_in_if e sc_signal_if_out per gli accessi in lettura e scrittura, rispettivamente, al canale Il listato 2 riporta invece il modello del canale sc_signal che implementa queste interfacce.

protemplate
<class T> class sc_signal_in_if:virtual public sc_interface
{
   public :
   virtual const sc_event& value_changed_event() const=0;
// get the value changed event
   virtual const T& read() const=0;
// read the current value
}

template <class T> class sc_signal_out_if:virtual public sc_interface
{
   public :
    virtual void write (const T&) = 0;
// write the new value
}
Listato 1
template <class T> class sc_signal:public sc_signal_
in_if<T>,
public sc_prim_channel
{
public :
  virtual const sc_event& default_event() const
// get the default event
  { return m_value_changed_event;}

 virtual const T& read() const
  { return m_cur_value;}

 virtual void write(const T& value_)
  {
  m_new_value= value_;
  if (!(m_new_value==m_cur_value))
  request_update();
  // call request/update method
    }
  protected:
    virtual void update()
  {
    if ( !(m_new_value==m_cur_value))
   {
   m_cur_value = m_new_value;
   m_value_changed_event.notify(SC_ZERO_TIME);
   }
}
T m_cur_value;
T m_new_value;
sc_event m_value_changed_event;
}
Listato 2

Il tipo di oggetto supportato dal canale è ereditato mediante templating. Come si può vedere, due oggetti di questo tipo sono definiti nell’area protetta; servono a contenere  rispettivamente  il valore corrente ed il prossimo valore, ovvero quello che dovrà essere aggiornato durante la fase di update del ciclo di simulazione Il metodo write() di accesso in scrittura al canale, quindi, dapprima memorizza il prossimo valore nella corrispondente variabile inter na; quindi, se è diverso dal valore corrente,  esegue una chiamata alla funzione request_update() del kernel. Durante la fase di update, questa richiama il metodo update() del canale; tale metodo copia il prossimo valore nel valore attuale e notifica l’occorrenza di un evento, che può essere restituito dalla funzione default_event().   Il metodo di lettura read(), invece, ritor na il  valore attuale. La possibilità di definire canali primitivi custom è una delle più interessanti proprietà del linguaggio SystemC in quanto permette di creare strutture estremamente veloci in termini di tempi di simulazione; tuttavia, a causa della necessità di definire un paradigma di gestione dei dati di tipo evaluate_update da utilizzare nelle descrizioni concorrenti dei sistemi, non sempre appare di facile realizzazione. E’ interessante notare, ad esempio, come il  semplice modello comportamentale della FIFO che è stato presentato nella precedente puntata, non supportando tali metodi, può condurre a degli errori in situazioni particolari. Si consideri, in particolare,  il caso in cui due processi attivati dallo stesso evento accedano uno in lettura e l’altro in scrittura alla memoria; in queste condizioni se il processo di lettura esegue una chiamata a num_available(), potrebbe trovare un risultato diverso a seconda che il processo di scrittura sia già stato eseguito o meno. Per la natura del kernel di simulazione, quest’ultima condizione non è predicibile né ripetitiva. Per questo, in molti casi, si preferisce evitare di creare canali primitivi custom e, quindi, si definiscono canali gerarchici, come descritto nel prossimo paragrafo, basandosi su canali primitivi nativi del linguaggio SystemC.

Canali gerarchici

Una delle principali caratteristiche del SystemC che deriva direttamente dalle proprietà del linguaggio C++ è la possibilità di descrivere sistemi complessi a diversi livelli di astrazione. Una delle principali applicazioni risiede nella modellizzazione di protocolli di comunicazione complessi; mediante l’utilizzo di canali gerarchici si è in grado di creare una descrizione più elegante ed efficiente di quanto sia possibile con i tradizionali linguaggi di descrizione hardware. I canali gerarchici  sono in realtà dei moduli che contengono al loro inter no moduli membro, canali e processi per implementare  i metodi delle interfacce esterne supportate. Nella puntata precedente [1]abbiamo mostrato come descrivere un’interfaccia FIFO mediante un canale primitivo che implementa in maniera blocking le interfacce fifo_write_if<> e fifo_read_if<>; la memoria è descritta come un semplice array di dati e due indici interi sono utilizzati per puntare alle locazioni in scrittura e lettura. In alter nativa, abbiamo presentato l’implementazione del modello della FIFO inclusa nello standard. Quindi abbiamo discusso un semplice esempio di comunicazione di tipo producer/consumer basato su FIFO. L’intera descrizione è stata presentata ad un livello di astrazione piuttosto elevato e non facendo riferimento ad alcuna realizzazione fisica; la sincronizzazione dei processi di lettura e scrittura in condizioni bloccanti è stata basata su eventi. Supponiamo, ora, di voler raffinare  il modello della FIFO tendendo verso una descrizione RTL; un esempio è riportato nel listato 3.

template class<T> class hw_fifo:public sc_module
{
public:
sc_in<bool> clk;
sc_in<T> data_in;
sc_in<bool> valid_in;
sc_in<bool> ready_out;
sc_out<T> data_out;
sc_out<bool> valid_out;
sc_in<bool> ready_in;
SC_HAS_PROCESS(hw_fifo);
hw_fifo(sc_module_name name, unsigned size):
               sc_module(name), _size(size)
{
asser(size>0)
_first = _items = 0;
_data = new T[_size];
SC_METHOD(main);
Sensitive << clk.pos();
ready_out.initialize(true);
ready_in.initialize(false);
}
~hw_fifo { delete[] _data; }

protected :
 void main ()
{
  if ( valid_in.read() && reay_out.reat() )
  {
      _data[(_first + _items) % size] = data_in;
// store new data intem into FIFO
       ++_items;
   }
   if ( ready_in.read() && valid_out.read() )
   {
       — _items;
       _first = (_first + 1) % size;
// discard data item that was just read from FIFO
  }
  ready_out = (_items < _size);
  valid_out = (_items > 0);
  data_out = _data[_first];
 }
unsigned _size, _first, _items;
T* _data;
}
Listato 3

Il modello presenta porte che implementano interfacce di tipo sc_signal_in_if e sc_signal_if_out per descrivere  i segnali di ingresso uscita; non può quindi essere direttamente connesso ai moduli producer e consumer descritti nell’esempio di riferimento  i quali supportano i metodi di acceso al canale FIFO. Devono essere costruiti in una logica che in qualche modo adatti le diverse interfacce; questa, unitamente al modulo che include il modello RTL, definisce un canale gerarchico che può essere direttamente sostituito al modello primitivo della FIFO. Il listato 4 riporta la descrizione del canale gerarchico in oggetto.

template class<T> class hw_fifo_wrapper : public
sc_module, public fifo_write_if<T>, public
fifo_read_if<T>
{
public :
  sc_in<bool> clk;
protected :
  // embedded channel
  sc_signal<T> write_data;
  sc_signal<bool> write_valid;
  sc_signal<bool> write_ready;
  sc_signal<T> read_data;
  sc_signal<bool> read_valid;
  sc_signal<bool> read_ready;
  // embedded module
  hw_fifo<T> hw_fifo;
public :
  hw_fifo_wrapper(sc_module_name name, unsigned
size):sc_module(name), hw_fifo(“hw_fifo”, size)
{
  hw_fifo.clk(clk);
  hw_fifo.data_in(write_data)
  hw_fifo.valid_in(write_valid)
  hw_fifo.ready_out(write_ready);
  hw_fifo.data_out(read_data)
  hw_fifo.valid_out(read _valid)
  hw_fifo.ready_in(read _ready);
}
virtual void write(const T& data)
{
  write_data = data;
  write_valid = true;
  do { wait (clk_> posedge_event()); }
  while (write_ready!= true);
   write_valid = false;
}
virtual T read()
{
  read_ready= true;
  do { wait (clk_> posedge_event()); }
  while (read_valid!=true);
  read_ready = false;
  return read_data.read();
}
virtual void read (T& d) { d = read()}
virtual int num_available() const
{ assert (0); return 0;}
}
Listato 4

Come si vede, viene definito un modulo di tipo hw_fifo_wrapper<>; all’inter no di questo, mediante  il costruttore, vengono instanziati un modulo di tipo FIFO e dei canali interni di tipo sc_signal_in_if e sc_signal_if_out che si connettono direttamente alle porte della memoria. Inoltre sono implementati  i metodi write() e read() ereditati dalle interfacce fifo_write_if <> ed fifo_read_if<>. Supportando quindi le stesse interfacce, il  canale gerarchico hw_fifo_wrapper<> può facilmente essere sostituito al canale primitivo fifo<>; il listato 5 mostra, nel caso dell’esempio presentato nella precedente puntata un’applicazione producer/consumer, le modifiche che è necessario apportare al codice.

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);
  }
};
class top : public sc_module
{
  public:
  hw_fifo_wrapper <char> fifo_inst; // changed
  producer prod_inst;
  consumer cons_inst;
  sc_clock clk; // added
  top(sc_module_name name) :
  sc_module(name),
  fifo_inst(“Fifo”, 32),
  prod_inst(“Producer”),
  cons_inst(“Consumer”),
  clk(“clock”, 1, SC_NS) // added
  {
    prod_inst.out(*fifo_inst);
    cons_inst.in(*fifo_inst);
    fifo_inst.clk(clk);  // added
}};
Listato 5

Come si vede, la possibilità di definire canali gerarchici permette di creare modelli equivalenti tra loro ma a diversi livelli di astrazione, che risultano quindi facilmente intercambiabili nella descrizione del sistema a seconda delle necessità.

Il modello comportamentale della FIFO.

Figura 1: Il modello comportamentale della FIFO.

Conclusioni

Si chiude con questa puntata il nostro tutorial sul SystemC. Nel corso delle quattro puntate, abbiamo introdotto in linea generalei i diversi vantaggi  del linguaggio rispetto ai tradizionali metodi di descrizione HDL; abbiamo visto nella pratica come creare dei modelli attraverso la descrizione di moduli e processi; abbiamo introdotto i concetti di interfacce, canali e porte come metodi per la connessione dei moduli ed approfondito il principio di funzionamento del kernel di simulazione ed i tipi di dati SystemC.