Préambule

Objectifs

Vous connaissez les éléments structurels de SystemC (comment décrire un module sous forme de briques reliées les unes aux autres).

Ce chapitre a pour objectif de vous présenter les différents éléments fonctionnels de SystemC, autrement dit comment représenter la fonctionnalité d'un module autrement que par un assemblage de sous-boîtes. Les éléments fonctionnels sont

 

A la fin de ce chapitre,

 

Plan du chapitre

  1. On commencera par étudier les objets permettant de synchroniser les processus : les événements.
  2. Puis on passera à une description généraliste des processus, qui nous ammènera à étudier les deux sortes qui existent :
  3. les SC_METHOD
  4. les SC_THREAD
  5. les SC_CTHREAD
Back to top

Evenements

Définition

Les événements sont les objets sur lesquels se base toute la synchronisation des processus. Ils déterminent quand est-ce qu'un processus doit être déclenché ou réveillé.

Il peuvent représenter des événements concrets (changement d'état d'un signal), ou abstraits.

Les événements sont des objets très simples, disposant de très peu de méthodes. Tout ce qu'on peut en faire c'est :

 

Vous avez déjà vu comment on met des signaux dans la liste de sensibilité des processus. En fait, ce que fait la méthode sensitive (et ses dérivés), c'est récupérer un événement associé au signal en question, et c'est cet événement qui est placé dans la liste de sensibilité. L'événement par défaut des signaux est inactif a plupart du temps, mais se trouve activé (on dit "notifié) lors d'un changement d'état du signal. Autrement dit, quand un signal change d'état, son événement associé est notifié, et cela réveille le processus qui l'a dans sa liste de sensibilité.


Attention, il faut bien distinguer l'événement, objet C++, de son occurrence (les instants où il est notifié) : quand on dit "le changement d'état d'un signal cause un événement", il faut en fait comprendre "le changement d'état d'un signal cause la notification de l'événement associé". Les événements sont des objets C++ qui ont la même durée de vie que l'objet auquel ils appartiennent. Mais ils se trouvent généralement inactifs, et, à certains cycles, notifiés.

Classe et méthodes

Les événements sont des instances de la classe sc_event. On les créé rarement soi-même, sauf quand on crée un nouveau type de canal, ou qu'on veut rendre un processus sensible à autre chose qu'un signal.

Un canal peut avoir plusieurs événements associés. Par exemple, les signaux ont un événement par défaut qui est notifié à chaque changement, un événement pour les fronts montant, un événement pour les fronts descendant.

On récupère l'événement associé à un canal par une des méthodes décrites au chapitre sur les interfaces :

Les événements possèdent les méthodes suivantes :

 

Exemple :

sc_event e1, e2, e3;

sc_time t_zero(0, SC_NS);
sc_time t_10(10, SC_NS);

// Notifications immédiates de e1
e1.notify();

// Notification de e2 au prochain delta_cycle
// équivalent à e2.notify(SC_ZERO_TIME);
e2.notify(t_zero);

// Notification de e3 dans 10ns
e3.notify(t_10);

// Annulation de e2
e3.cancel();

 

 

En résumé :

Les événements sont les objets qui servent à synchroniser les processus (les déclencher, les réveiller).

Ce sont des objets C++, qui sont soit dans un état inactif, soit notifié.

Les listes de sensibilité sont en faites construites à partir des événements par défaut des canaux, et c'est la notification de ces événements qui réveillent les processus.

On peut aussi instancier manuellement des événements, et les notifier ou les annuler.

 

On peut maintenant étudier les différents types de processus, comment on les instancie, comment on modifie à la volée leur liste de sensibilité.

 

 

Back to top

Processus

Les processus de SystemC se comportent comme les processus de Verilog ou VHDL. Ils décrivent la fonctionnalité d'un module.

Ce sont des fonctions membres des modules, de type void f(void). On ne les appelle pas directement, c'est le scheduler SystemC qui se charge de les appeler en se basant sur leur liste de sensibilité. Un processus peut appeler une méthode membre de son module, mais pas un autre processus.

Les processus utilisent les événements et les canaux pour communiquer entre eux. Il est possible de faire communiquer des processus par des variables, mais c'est fortement déconseillé : les variables sont affectées immédiatement. L'ordre d'exécution des processus étant indéterminé, cela peut conduire à des conditions de course (race condition), de l'indéterminisme, des comportements erratiques, etc.

Un processus peut accéder directement à un canal qui fait partie du même module, mais est obligé de passer par un port pour communiquer avec l'extérieur du module.

Il existe deux types de processus en SystemC : SC_METHOD et SC_THREAD. En fait, il en existe un troisième, SC_CTHREAD, qui est une spécialisation des SC_THREAD permettant de modéliser exclusivement de la logique synchrone. Nous parlerons principalement des SC_METHOD et SC_THREAD, et mentionnerons brièvement les SC_CTHREAD.

Un module peut avoir un ou plusieurs processus, de même type ou de type différent.

Les trois types de processus sont instanciés en 4 étapes :

  1. déclaration du processus : c'est une méthode du module, comme toute méthode il doit avoir une déclaration et une implémentation
  2. enregistrement : les processus ne sont pas appelés directement, c'est le scheduler SystemC qui les appelle. Mais il doit savoir quelle méthode appeler, et quel est son type (SC_METHOD ou SC_THREAD). C'est le rôle de cette étape.
  3. déclaration de la liste de sensibilité par défaut : le scheduler doit aussi savoir quand appeler ou réveiller le processus. Pour cela on attribue au processus une liste de sensibilité par défaut. Une fois que la simulation a démarré, les processus ont le droit de modifier leur propre liste de sensibilité.
  4. implémentation : comme toutes les méthodes, il faut fournir une implémentation. Préférez les fichiers séparés pour les déclarations et implémentations.

L'objectif des prochaines section est d'étudier chaque type de processus, leurs comportements et objectifs étant différents.

On commencera par les SC_METHOD, puis on continuera avec les SC_THREAD. On étudiera enfin les SC_CTHREAD.

 

 

Back to top

SC_METHOD

Définition

Les SC_METHOD :

 

Ceci est très important : le scheduler SystemC est multi-thread. Mais les SC_METHOD ne sont pas des threads. Ce sont des fonctions normales, que le scheduler appelle les unes après les autres, quand un événement de leur liste de sensibilité est notifié. Quand le SC_METHOD se termine ( return() ), il redonne la main au scheduler. Si, pour une raison ou pour une autre, le SC_METHOD ne retourne pas (s'il est bloqué, s'il est dans une boucle infinie), le scheduler est bloqué, la simulation n'avance plus !

Les variables locales des SC_METHOD sont donc ré-initialisées à chaque entrée dans le processus (à moins de les avoir déclarées statiques). Si le SC_METHOD a besoin de sauvegarder des informations, cela doit être fait dans des variables ou signaux membre du module.

Création des SC_METHOD

Comme tous les processus, leur création se fait en 4 étapes :

Déclaration

Un SC_METHOD est une méthode (fonction) du module qui ne prend pas d'argument et renvoie void. Il est déclaré comme toutes les autres méthodes.

Exemple :

SC_MODULE(mon_module) {
// déclaration des ports, signaux, ...

// déclaration d'un processus (pour l'instant rien ne spécifie son type)
void mon_sc_method();


....

}

 

Enregistrement

C'est ici qu'on va indiquer au scheduler SystemC que cette fonction est un SC_METHOD, et qu'il faudra donc l'appeler quand ce sera nécessaire. On utilise pour cela la macro SC_METHOD, qui prend en argument le nom de la fonction.

L'enregistrement de la fonction auprès du scheduler se fait dans le constructeur du module.

Exemple :

SC_MODULE(mon_module) {
// déclaration des ports, signaux, ...

// déclaration d'un processus
void mon_sc_method();

// constructeur
SC_CTOR(mon_module) {

// Enregistrement du processus comme un SC_METHOD
SC_METHOD(mon_sc_method);


// autres initialisations...
}

...

}

 

Déclaration de la liste de sensibilité par défaut

Il faut maintenant spécifier la liste de sensibilité du processus. Il pourra lui même la modifier au coup par coup lors de la simulation s'il le désire. Cela se fait dans le constructeur du module.

Pour cela, on spécifie à quel événement il est sensible à l'aide de sensitive.

Deux syntaxes sont possibles:

Elle prend en argument un événement, et l'ajoute à la liste de sensibilité du dernier processus enregistré. Elle peut aussi prendre en argument un signal, et en extrait alors automatiquement l'événement par défaut (qui est notifié à chaque changement d'état du signal).

Il est possible de filtrer les fronts montants ou descendants, uniquement pour les signaux sur 1 bit bien évidement (un bus n'a pas de "front montant"...). Pour cela, on a deux possibilités :

 

Remarques :
  • Les fonctions sensitive_pos et sensitive_neg ne sont plus supportées. Il semble qu'on puisse quand même les utiliser, mais avec les SC_METHOD uniquement. Ne pas les utiliser avec les SC_THREAD. Il vaut mieux les remplacer par sensitive << mon_signal.pos() et sensitive << mon_signal.neg().
     
  • Il vaut mieux filtrer les fronts lors de la déclaration de la liste de sensibilité plutôt que dans le corps du module. Cela accélère les simulations (surtout pour les SC_THREAD, étudiés plus tard).
     
  • Même si parfois on peut utiliser la construction sensitive << clock.posedge_event(), il vaut mieux la remplacer par sensitive << clock.pos(), qui n'oblige pas à ce que le port soit relié au moment où l'instruction est rencontrée.

 

 

Si un processus doit être sensible à plusieurs événement, on peut soit appeler plusieurs fois sensitive, soit profiter de la notation de flux << pour enchaîner les signaux.

Par défaut, les processus sont tous exécutés une fois au début de la simulation. Si on ne le souhaite pas, il suffit d'appeler la fonction dont_initialize() après la déclaration de la liste de sensibilité (toujours dans le constructeur).

Exemple :

SC_MODULE(mon_module) {


// déclaration des ports, signaux, variables et d'une fifo
sc_in<bool> enable;
sc_fifo<int> fifo;
sc_event ev1;
sc_port<sc_signal_in_if<bool> > reset;

// déclaration d'un processus
void mon_sc_method();

// constructeur
SC_CTOR(mon_module) {

// Enregistrement du processus comme un SC_METHOD
SC_METHOD(mon_sc_method);

// Déclaration de sa liste de sensibilité
sensitive << ev1 << enable << reset;
sensitive << fifo.data_read_event();

// Si on ne veut pas l'exécuter au début de la simulation
dont_initialize();

// Enregistrement des autres processus
...


}

...

}

 

Implémentation

L'implémentation suit les mêmes règles que les fonctions normales. Quand le processus a fini d'exécuter ce qu'il a à faire, il effectue un return() pour redonner la main au simulateur.

 

Modification dynamique de la liste de sensibilité des SC_METHOD

Il est parfois désirable de mettre un processus en veille (attente d'un événement). Or, un SC_METHOD ne peut pas être mis en attente, sinon il bloque le scheduler. Il existe quand même un moyen d'implémenter une simili mise en veille, en remplaçant temporairement sa liste de sensibilité par une autre, et en redonnant la main au scheduler.

Le processus ne s'exécute plus : tout se passe comme s'il était en veille, mais en réalité il est terminé. Lors de la notification de la nouvelle liste de sensibilité, il est redémarré (depuis le début). La nouvelle liste de sensibilité est alors oubliée, et il reprend sa liste par défaut.

La redéfinition de la liste de sensibilité se fait au moyen de la fonction next_trigger(). La fonction next_trigger() ne force pas le processus à redonner la main au simulateur. C'est au programmeur de faire éventuellement un return() après le next_trigger(). Si plusieurs next_trigger() sont exécutées, la dernière gagne (les précédentes sont ignorées). Si on appelle next_trigger() sans argument, le processus reprend sa liste de sensibilité par défaut. S'il n'en avait pas, il n'est plus jamais exécuté.

La fonction next_trigger() accepte de multiplex arguments :

 

Exemple :

void mon_module::mon_sc_method() {
 // fait quelques actions
 ...

 // modification de la liste de sensibilité
 // au prochain coup, il ne sera sensible qu'à e1
 next_trigger(e1);

 // au prochain coup, il faudra attendre que e1 et e2 aient été notifiés pour 
 // que le processus soit déclenché
 next_trigger(e1 & e2);

 // le processus sera déclenché dans 10ms
 next_trigger (10, SC_MS);

 // idem
 sc_time t_10(10,SC_MS);
 next_trigger(t_10);

 // au prochain coup, il sera déclenché sur e1 ou e2. Mais attend au maximum 10ms. 
 // Si 10ms se sont écoulées sans que e1 ou e2 soient notifiés, 
 // déclenche quand même le processus
 next_trigger(t_10, e1 | e2);

}

 

Quand utiliser un SC_METHOD ?

Les SC_METHOD sont utilisés pour :

Exercice

 

 

En résumé :

Les SC_METHOD sont des fonctions d'un module, appelées successivement par le scheduler SystemC quand leur liste de sensibilité est notifiée.

Ils s'exécutent dans le contexte du processus, et ne doivent donc pas être bloquant (attention aux boucles infinies !)

Ils sont déclarés dans la déclaration du module, enregistrés dans le constructeur, implémentés à part. Leur liste de sensibilité est spécifiée dans le constructeur.

Ils ont la possibilité de modifier temporairement leur liste de sensibilité, cette modification n'étant valable que pour le prochain appel.

 

Pour bien comprendre pourquoi on a insisté que les SC_METHOD s'exécutent dans le contexte du scheduler, il peut être intéressant d'étudier les SC_THREAD, qui ont un comportement tout à fait différent.

Back to top

SC_THREAD

Définition

Les SC_THREAD :

 

La différence avec les SC_METHOD doit maintenant être claire :

 

Un SC_THREAD est mis en veille par la fonction wait(). Le scheduler SystemC ne fait avancer les delta_cycle que lorsque tous les SC_THREAD sont en veille. Si un SC_THREAD se termine (return() ), il ne s'exécute plus jamais. Donc s'il doit ête exécuté de façon cyclique, à chaque coup d'horloge par exemple, il faut l'implémenter sous forme d'une boucle infinie (while(1) {...} ), et appeler wait(clk) là où c'est nécessaire dans cette boucle.

Lorsqu'un SC_THREAD exécute wait(), il est mis en veille. Il est réveillé sur notification de sa liste de sensibilité et reprend son exécution là où il en était. Ses variables locales ont bien entendu gardé leur valeur.

La fonction wait() peut être exécutée par le SC_THREAD, ou bien par une fonction qui a été appelée par ce SC_THREAD (méthode du module, ou méthode d'un canal utilisé par le SC_THREAD). Les SC_THREAD peuvent donc utiliser les lectures et écriture bloquantes des sc_fifo, alors que les SC_METHOD ne peuvent pas !

 

Création des SC_THREAD

Le processus de création des SC_THREAD est exactement le même que celui des SC_METHOD, en utilisant à la place la macro SC_THREAD... Nous ne la détaillerons donc pas.

 

Modification dynamique de la liste de sensibilité des SC_THREAD

Les SC_THREAD peuvent aussi modifier leur liste de sensibilité au vol. Sans surprise, cela est fait par la fonction wait() :

 

Quand utiliser un SC_THREAD ?

Les SC_THREAD sont utilisés pour :

 

Exemple : machine à état modélisée par un SC_THREAD

void mon_module::mon_thread() {
while (true) {
feux = vert;
wait(appel_bouton);

// attend un peu
for(int i=0; i<50; i++)
wait();
feux = orange;

// attend un peu
for(int i=0; i<50; i++)
wait();
feux = rouge;

// attend beaucoup
for(int i=0; i<5000; i++)
wait();

// repasse au vert : retour au début de la boucle
}
}

 

Vous connaissez maintenant le principe des SC_THREAD. Avant de résumer tout ça, étudions brièvement les SC_CTHREAD.

 

 

Back to top

SC_CTHREAD

Définition

Les SC_CTHREAD sont des cas particuliers des SC_THREAD. Leur nom signifie Clocked THREAD : threads synchrones. Ils sont utilisés pour modéliser des thread sensibles uniquement à un front d"horloge. Comme la plupart des dispositifs actuels sont des circuits synchrones, les SC_CTHREAD peuvent être intéressants.

Création des SC_CTHREAD

Un SC_CTHREAD est, comme les autres processus, une fonction de type void f()

Enregistrement des SC_CTHREAD

Un SC_CTHREAD est enregistré auprès du moteur de simulation par la macro SC_CTHREAD, en lui passant en argument le nom de la fonction associée ainsi que l'événement correspondant au front d"horloge sur lequel le processus est synchrone.

SC_MODULE(mon_module) {
 // déclaration des ports, signaux, ...
 sc_in<bool> clock;
 
 // déclaration d'un processus (pour l'instant rien ne spécifie son type)
 void mon_sc_cthread();
 
 // constructeur du module
 SC_CTOR(mon_module) {
 // Enregistrement de la fonction comme étant un SC_CTHREAD
 // sensible au front montant de l'horloge
 SC_CTHREAD(mon_sc_cthread, clock.pos());
 }
 ...
};

sensitive n'existe pas pour les SC_CTHREAD.

Fonctionnement des SC_CTHREAD

Comme les SC_THREAD, les SC_CTHREAD sont des threads Unix indépendants du simulateur. Une fois qu'ils ont exécuté un return(), ils sont morts et ne sont plus jamais exécutés.

La mise en veille se passe comme pour le SC_THREAD, à l'aide de la fonction wait(), mais seules deux formes sont supportées :  


Exemple

sc_in<bool> clock;

SC_CTOR(mon_module) {
  SC_CTHREAD(mon_cthread, clock.pos());
}

void mon_cthread() {
  while(1) {
    wait(); // Attente du prochain front d'horloge
    ... 
    wait(3); // Attente de 3 cycles (réveil au troisième front)
   ...
  }
}
Vous connaissez maintenant le principe des SC_CTHREAD. Avant de résumer tout ça parlons de reset.

 

Back to top

Gestion du Reset

Les SC_THREAD et les SC_CTHREAD sont implémentés sous la forme d'une boucle infinie avec des point d'arrêt (wait()).

Pour prendre en compte un éventuel signal de remise à zéro (reset), on doit tester au réveil du processus si le reset est actif et exécuter un code de remise à zéro. Ceci peut se faire soit en utilisant des goto (peu élégants en C / C++), et en général conduit à du code compliqué avec plein de if else partout...

 

sc_in<bool> clock;
sc_in<bool> reset; //reset synchrone, actif haut

SC_CTOR(mon_module) {
  SC_THREAD(mon_thread)
  sensitive << clock.pos();
  // si reset asynchrone l'ajouter à la liste de sensibilité
}

void mon_thread() {
  // Au premier front d'horloge, on teste quand même si le reset est actif ou non
  if (reset) {
  reset_actions :
   ... // Initialisations
  }
  while(1) {
    wait(); // Attente du prochain front d'horloge
    // tester si le reset est actif
    if (reset == true)
       goto reset_actions;
    ...     // le reste des actions synchrones
  }
}

 

La version 2.3.0 de SystemC offre la possibilité de définir explicitement les signaux permettant de remettre à zéro un processus (Jusqu'à la version 2.2.0, seule la remise à zéro synchrone des SC_CTHREAD était possible).

La définition des signaux de reset se fait à l'aide des fonctions reset_signal_is() et async_reset_signal_is(). Elles prennent en argument le nom d'un signal de reset, et l'état auquel il est actif (haut ou bas).

Comme sensitive, les fonctions reset_signal_is et async_reset_signal_is  porte sur le dernier processus à avoir été enregistré.

sc_in<bool> clock;
sc_in<bool> reset;

SC_CTOR(mon_module) {
  SC_THREAD(mon_thread)
  sensitive << clock.pos();
  reset_signal_is(reset,true);
  // mon_thread dispose maintenant d'un signal de reset synchrone, actif haut
}

void mon_thread() {
  // Au premier front d'horloge, on teste quand même si le reset est actif ou non
  if (reset) {
   ... // Initialisations
  }
  while(1) {
    wait(); // Attente du prochain front d'horloge
            // Si reset est actif on redémarre le processus
    ...     // le reste des actions synchrones
  }
}

Fonctionnement

 

Les fonctions reset_signal_is et async_reset_signal_is sont aussi utilisable avec le SC_METHOD. Dans ce cas, un reset actif remettra juste la liste de sensibilité par défaut (comme next_trigger()) et si le reset est asynchrone, ça se passera comme s'il appartenait aussi à la liste de sensibilité.

 

On va maintenant résumer tout ça, avant de passer à la suite ! 

 

Back to top

En résumé

Ce chapitre vous a présenté comment décrire la fonctionnalité des modules en terme de processus.

Les processus peuvent être de trois type :

  • SC_METHOD qui est exécuté à chaque notification de sa liste de sensibilité, dans le contexte du scheduler (il ne doit donc pas bloquer)
  • SC_THREAD qui est exécuté une seule fois, mais peut être mis en veille, et réveillé à chaque notification de sa liste de sensibilité.
  • SC_CTHREAD qui est un SC_THREAD particulier, totalement synchrone.

 

Les processus :

  • sont déclarés dans le corps du module, comme des fonctions void f(),
  • sont enregistré comme SC_THREAD, SC_CTHREAD ou SC_METHOD dans le constructeur du module
  • se voient attribuer une liste de sensibilité par défaut dans le constructeur du module à l'aide de la fonction sensitive(sauf pour les SC_CTHREAD)
  • sont implémentés là où c'est le plus pratique (dans un fichier séparé...)

 

 

Vous savez maintenant comment écrire des modules complets.

Il ne reste qu'un petit détail à régler : comment lancer et contrôler la simulation, et éventuellement comment débugger !

C'est l'objectif du prochain chapitre.

Back to top