Introduction à SystemC

Les canaux de communication

Tarik Graba

2023-2024

Au-delà des simples signaux

Interface et canal

En SystemC on peut définir des canaux de communication complexes.

L’interface de ces canaux est séparée de la définition de leur comportement pour permettre de faire évoluer les canaux de communication sans modifier ce qui se trouve des deux cotés du canal.


La séparation entre la définition de l’interface et son comportement se fait en utilisant le concept d’interfaces de la programmation orientée objet.

En C++, le concept d’interface utilise ce qu’on appelle des classes abstraites (virtuelles pures).

Exemple

Comment définir des interfaces en C++ ?

#include <iostream>
using namespace std;

class MyInterface {
public:
   // "=0" veut dire implémentation obligatoire
   virtual void hello()   = 0;
};

class SimpleImpl : virtual public MyInterface {
public:
   virtual void hello() override
   {
      cout << "Hi" << endl;
   }
};

class CplxImpl : virtual public MyInterface {
public:
   virtual void hello() override
   {
      cout <<  "Hi " << message << endl;
   }
   CplxImpl(string s): message(s) {}

private:
   const string message;
};

int main()
{
   // MyInterface   x; // ceci est une erreur car la classe est abstraite

   MyInterface * o[2];

   o[0] =  new SimpleImpl();
   o[1] =  new CplxImpl("folks");

   for (int i=0; i<2; i++)
      o[i]->hello();

   return 0;
}

Pour garder l’isolation entre les éléments d’une hiérarchie, les ports doivent permettre de rendre accessible les méthodes des interfaces dans un module.

Pour avoir un minimum d’interopérabilité, SystemC définit plusieurs types :


sc_interface

Définit le minimum de méthodes à implémenter pour fonctionner avec le simulateur :

Toute interface doit hériter de ce type pour pouvoir s’intégrer dans une simulation.

sc_prim_channel

Définit en plus les méthodes permettant l’interaction avec le moteur de simulation :

sc_port

Permet de déclarer un port pour une interface particulière. C’est une classe template dont l’un des paramètres est l’interface utilisée. Les autres paramètres correspondent au nombre de canaux qu’on peut y connecter (par défaut exactement 1).

Exemple d’utilisation

Prenons le temps de regarder le code de l’exemple.


#include <systemc.h>

// Une interface pour l'écriture
class tag_write_if : virtual public sc_interface
{
   public:
      virtual void write(sc_uint<8> i) =0;
};

// Une interface pour la lecture
class tag_read_if : virtual public sc_interface
{
   public:
      virtual sc_uint<16> read() =0;
};

// notre canal personnalisé, implémente les deux interfaces
// ce canal est semblable au sc_signal avec:
//    - des données de taille fixe
//    - un tag ajouté pour chaque nouvelle donnée
class tagged_bus : public tag_write_if, public tag_read_if, public sc_prim_channel
{
   sc_uint<8> tag;
   sc_uint<8> cur_d, new_d;
   sc_event   m_ev;

   public:
   SC_CTOR(tagged_bus) {
      tag   = 0;
      cur_d = 0;
      new_d = 0;
   }
   // dans tag_write_if
   virtual void write(sc_uint<8> i) {
      new_d = i;
      // on demande à ce qu'update soit appelé
      request_update();
   }
   // dans tag_read_if
   virtual sc_uint<16> read() {
      return (tag,cur_d);
   }
   // dans sc_interface
   virtual const sc_event& default_event() const {
      return m_ev;
   }
   // dans sc_prim_channel
   virtual void update() {
      if (cur_d != new_d) {
         cur_d = new_d;
         tag++;
         m_ev.notify(SC_ZERO_TIME);
      }
   }
};

SC_MODULE(W) {
   // un port en sortie n'implémente que l'interface d'écriture
   sc_port<tag_write_if> p_o;

   SC_CTOR(W) {
      SC_THREAD(loop);
   }
   void loop () {
      sc_uint<8> v = 1;
      while(v) {
         // on appelle la méthode write de l'interface
         p_o->write(v);
         v = v<<1;
         wait(10, SC_NS);
      }
   }
};

SC_MODULE(R) {
   // un port en entrée n'implémente que l'interface de lecture
   sc_port<tag_read_if> p_i;

   SC_CTOR(R) {
      SC_METHOD(loop);
      // Utilise le default_event
      sensitive << p_i;
   }
   void loop () {
      // on appelle la méthode read de l'interface
      sc_uint<16> t = p_i->read();
      cout << name() <<" --> tag: " << t(15,8) << " val: " << sc_bv<8>(t(7,0))<< endl;
   }
};

int sc_main(int argc, char * argv[])
{

   tagged_bus b("t_bus");

   W w("writer");
   w.p_o(b);

   R r("reader");
   r.p_i(b);

   sc_start();

   return 0;
}

Et pour les sc_signal

Les sc_signal utilisent ces mêmes notions d’interfaces et canaux et de ports.

L’interface

Pour les signaux les deux interfaces suivantes sont définies :


Ces deux interfaces sont définies comme des classes template, où le template T est le type des données transporté.

Des spécialisations de ces interfaces existent pour les types bool et sc_logic. Elles ajoutent ce qu’il faut pour les évènements sur front.


Les ports sc_in, sc_out


Ces ports spécialisés ajoutent des surcharges d’opérateurs pour permettre simplement d’écrire et de lire dans le signal. Aussi, on ne peut y connecter qu’un seul signal.


Le sc_signal

Est défini comme :

template <class T,
          sc_writer_policy WRITER_POLICY = SC_ONE_WRITER>
class sc_signal
: public sc_signal_inout_if<T>, public sc_prim_channel
  { ... };


Des canaux standards

En plus des sc_signal, dans la bibliothèque, un certain nombre de canaux standards sont définis.

Les sc_buffer<T>

Un sc_buffer est équivalent à un sc_signal et implémente la même interface.

La seule différence, vient du fait que pour un sc_buffer il y a un évènement notifié à chaque écriture alors que pour un sc_signa il faut que la valeur change.


Travail à faire :

Écrire un exemple de code mettant en évidence la différence de comportement entre sc_signal et sc_buffer


Les sc_fifo<T>

Permet d’instances des fifos dont la taille est définie à l’instanciation.

Elles implémentent les interfaces :

Des ports spéciaux sont aussi prévus :


L’interface sc_fifo_in_if<T> fournit :

L’interface sc_fifo_out_if<T> fournit :

Attention comme les méthodes bloquantes de lecture et d’écriture font appel à wait() pour suspendre le processus qui les appelle, il faut donc prendre des précautions quand on les utilise dans des SC_METHOD.

Le constructeur d’une sc_fifo<T> prend en argument la taille de la fifo. La taille par défaut est de 16. De plus l’opérateur d’affectation a été surchargé pour appeler les méthodes bloquantes de lecture et d’écriture.

Exemple :

   // une fifo de 16 éléments
   sc_fifo<int> A;
   // une fifo de 32 éléments
   sc_fifo<int> B(32);

   // deux écritures bloquantes
   A.write(1);
   B = 1;

   // deux lectures bloquantes
   int x;
   x = A;
   x = B.read();

Exemple plus complet :

#include <systemc.h>
SC_MODULE(A) {
   sc_fifo_out<int> out;
   void loop() {
      int i = 0;
      for(;;){
         // write est obligatoire
         out.write(i);
         i++;
         wait(33, SC_NS);
      }
   }
   SC_CTOR(A) { SC_THREAD(loop); }
};
SC_MODULE(B) {
   sc_fifo_in<int> in;
   void loop() {
      wait(300, SC_NS);
      for(;;){
         // in.read() appelle in->read()
         cout << "Lecture 1 : " << in->read() << "@ " << sc_time_stamp() << endl;
         cout << "Lecture 2 : " << in.read()  << "@ " << sc_time_stamp() << endl;
         wait(55, SC_NS);
      }
   }
   SC_CTOR(B) { SC_THREAD(loop); }
};

int sc_main(int, char **) {
   // une fifo de 10 entiers
   sc_fifo<int> fifo(10);

   A a("modA");
   a.out(fifo);

   B b("modB");
   b.in(fifo);

   sc_start(300,SC_NS);
   cout 
      << "contenu de la fifo @" << sc_time_stamp() << endl
      << fifo << endl;

   sc_start(1,SC_US);
   cout 
      << "contenu de la fifo @" << sc_time_stamp() << endl
      << fifo << endl;

   return 0;
}

Travail à faire :

Écrire le code de deux modules s’échangeant des Pixels à travers une fifo. Les modules devront fonctionner à des cadences différentes tout en étant synchrones à une horloge clk générée au niveau supérieur. Par exemple :

Faites deux implémentations du producteur, l’une utilisant des SC_THREAD ou SC_CTHREAD et l’autre utilisant des SC_METHOD.


Les sc_mutex et sc_semaphore

La bibliothèque définit d’autres canaux standards permettant la synchronisation de SC_THREAD pour l’accès à des ressources partagées.


Le sc_mutex fournit les méthodes :

Le sc_semaphore prend en argument de constructeur le nombre de sémaphores qu’on peut prendre. Si ce nombre est égal à un, son comportement est équivalent à un sc_mutex.

Le sc_semaphore fournit les méthodes :

Attention

Il n’y a pas de ports dédiés à ces deux types de canaux. Si vous voulez utiliser ces canaux entre deux modules, il faudra passer par un sc_port générique exposant l’interface de ces deux type de canaux (respectivement sc_mutex_if et sc_semaphore_if).

Travail à faire :

Écrire le code d’un module dans lequel deux SC_THREAD concurrents s’exécutent à tour de rôle en utilisant un sc_mutex.

Un sc_mutex suffirait-il si on avait trois processus ?


sc_channel

SystemC définit la notion de canal hiérarchique. Un canal hiérarchique n’est en réalité qu’un sc_module qui, en utilisant entre autres des sc_export, peut se présenter comme un canal de communication complexe.

On peut donc y trouver des processus et modéliser les comportements du canal de communication.

Pour définir explicitement un canal hiérarchique on utilisera la classe sc_channel qui n’est qu’un alias de la classe sc_module.


Travail à faire :

En utilisant la classe sc_channel écrire le code d’un générateur d’horloge. Ce canal contiendra un sc_signal qui pourra être connecté à l’entrée d’un module de test à travers un sc_export.


Retour au sommaire du cours