Préambule

Objectifs

Ce chapitre a pour objectif de vous présenter les différents éléments structurels de SystemC :

 

A la fin de ce chapitre,

 

Plan du chapitre

  1. On commencera par l'objet le plus "petit" (interne) : les interfaces, point commun entre les canaux et ports.
  2. On continuera par les canaux, qui implémentent les interfaces.
  3. Puis on étudiera les ports, notamment les ports spécialisés (pré-définis)
  4. De là découlera la structure des modules...
Back to top

Interfaces

Définition

Un port d'un module est connecté à un canal (sauf dans le cas particulier où il est directement connecté au port d'un module parent). Les processus du module communiquent vers l'extérieur au travers de ces ports, en appelant les méthodes du canal auquel il est relié (ce canal est généralement un signal).

Une interface est un objet C++ ne contenant que les déclarations des méthodes du canal relié à un port, et donc accessibles par l'intérieur du module.

Le port est donc un trou dans un module, et l"interface un moyen de se mettre d'accord avec le canal qui y sera branché sur la manière de discuter.

Dit autrement, quand on accède à un canal depuis l'intérieur d'un module (à travers un port), on ne peut appeler que les méthodes de ce canal déclarées dans l'interface du port.

Quand on accède à un port, on accède en fait de manière cachée aux méthodes du canal qui lui est relié par l'intermédiaire de l'interface.

Pour en savoir plus...

 

Interfaces standards

Voici les interfaces standards définies par SystemC, pour les canaux atomiques (signal, fifo, mutex, sémaphore) :

sc_signal

Il existe deux interfaces pour les signaux : sc_signal_in_if pour la lecture seule des signaux (connectés à un port sc_in), et sc_signal_inout_if qui rajoute la notion d'écriture (signaux connectés à un port sc_out ou sc_inout).  

sc_signal_in_if<T> (pour les ports en lecture seule)

 

sc_signal_inout_if<T> (pour les ports aussi en écriture)

sc_buffer

Un buffer se comporte exactement comme un signal, à la différence près qu'un événement de changement d'état est notifié à chaque affectation, même si on lui affecte la même valeur que celle courante. Pour un sc_signal, le changement d'état n'est notifié que si la valeur change.

Il implémente les même interfaces que sc_signal.

sc_fifo

Comme pour les signaux, les fifo disposent de deux interfaces selon si on y accède en lecture ou en écriture.

sc_fifo_in_if<T> :

 

sc_fifo_out_if<T> :

sc_mutex

sc_mutex_if :

sc_semaphore

sc_semaphore_if :

 

 

En résumé :

Une interface est une classe C++ ne comportant que les déclarations des méthodes d'un canal auxquelles un port pourra accéder.

Les ports et les canaux sont des classes C++ qui héritent de leur classe de base et de celle de l'interface.
Le canal doit implémenter les méthodes en question.
Le port redirige les appels aux méthodes de son interface vers l'implémentation du canal.

 

Passons maintenant à l'étude des canaux.

 

 

Back to top

Canaux

Vous connaissez les signaux de VHDL ou Verilog. Les canaux sont une généralisation des signaux : un canal est fait pour transmettre de l'information entre un processus et un autre processus ou un port de module. En SystemC, cette information peut être extrêmement abstraite. Ca peut être un bit, un flottant, un packet ethernet, ou une information d'accès exclusif (exemple : "j'ai pris un sémaphore"). Les canaux ne sont pas forcément point à point : ils peuvent modéliser des bus, ...

Les signaux sont donc des types particuliers de canaux, correspondant à des bus ou des fils. SystemC en prédéfinit d'autres dont les principaux sont les buffers, les fifo, les sémaphores, les mutex. 

Les canaux sont utilisés :

 

Les canaux implémentent les interfaces. Quand un processus accède à un port, c'est en fait la méthode du canal correspondant qui est exécutée. Cette méthode s'exécute dans le contexte du processus. Cela une importance particulière pour les fifo : un processus qui fait une lecture bloquante sur un port connecté à une fifo peut se retrouver bloqué (suspendu). Bien que la méthode exécutée soit implémentée par la fifo, c'est le processus dans le contexte duquel elle s'exécute (le processus appelant) qui est suspendu.

Il existe deux types de canaux :

 

Utilisation

variables
Pour transmettre une information qui n'a pas besoin d'affectation différée et pour accélérer à tout prix les simulations.
L'utilisation des variables pour communiquer de l'information entre processus est souvent une mauvaise habitude.
 
canaux atomiques
Pour transmettre de l'information qui a besoin d'affectation différée, et que le moyen de communication ne peut pas être découpé en éléments plus petits (c'est le cas des signaux...).
Les canaux atomiques simulent moins vite que les variables, mais plus vite que les canaux hiérarchiques.
 
canaux hiérarchiques
Dans tous les autres cas : quand le moyen de communication a plusieurs ports, quand il comporte des processus multiples, quand aucun des canaux atomiques ne convient...

Nous ne occuperons ici que des canaux atomiques, les canaux hiérarchiques étant des modules, ils seront étudiés plus tard.

 

sc_signal<T>

Les signaux sont les principaux canaux atomiques. Ce sont des classes "templatisées" qui doivent donc être personnalisées par le type de la valeur sous-jacente transmise par le signal. On peut les voir comme des tuyaux, le T étant le type de liquide qui s'écoule dedans.

Exemples :

 

Les affectations des signaux sont toujours différées. Pour cela, ils possèdent trois membres : current_value, new_value et old_value. Une affectation modifie new_value, et à la fin du delta_cycle, si new_value est différente de current_value (au sens de "== renvoie false"), new_value est affectée à current_value et un événement est déclenché (on dit "notifié").

Les signaux ne peuvent être affectés que par un seul processus à la fois. Lors du démarrage de la simulation, une analyse est faite pour vérifier que chaque signal n'est connecté qu'à un seul port en écriture maximum. Le nombre de ports en lecture, par contre, n'est pas limité.

Les signaux implémentent l'interface sc_signal_inout_if. Ils implémentent donc automatiquement sc_signal_in_if (la première dérivant de la deuxième).

Les principales méthodes des signaux sont :

value_changed_event(), default_event()
renvoie une référence (pointeur de C++) à un événement qui peut alors être utilisé dans une instruction wait(). Cet événement est notifié (déclenché) au delta cycle suivant la modification de la valeur signal.
posedge_event(), negedge_event()
valide seulement pour les signaux de type bool ou sc_logic. Semblable à ci-dessus, mais seulement si le signal passe à true (resp false).
read() (ou "=" s'il apparait à droite du signe égal)
renvoie une référence à current_value
write(const T& val) (ou "=" s'il apparait à gauche du signe égal)
si val est différent de current_value, alors stocke val dans new_value, et schedule une mise à jour du signal.
posedge(), negedge()
renvoie un booléen, true si le signal a subi une transition vers true (resp. false) au delta_cycle précédent, false sinon.
event() 
renvoie un booléen, true si le signal a été modifié au delta-cycle précédent, false sinon.
 

Exemples d'utilisation :

sc_signal<bool> enable, control;
sc_signal<sc_lv<32> > data;
long valeur;

enable = 0;
valeur = data; // peut donner un résultat indéfini si data contient un 'X' ou 'Z'...
if(enable==control) {...}

sc_buffer<T>

Similaire à sc_signal, à une différence près : sc_signal ne déclenche un événement que si sa nouvelle valeur est différente de la valeur courante, alors que sc_buffer déclenche un événement d'affectation quelque soit la nouvelle valeur.

 

sc_signal_rv<int n>, sc_signal_resolved

Ce sont en fait des sc_signal<sc_lv<n> > et sc_signal<sc_logic>, autrement dit des signaux trois-états sur un ou plusieurs bits, avec une différence : ils peuvent être connecté à plusieurs ports en écriture à la fois. Ils sont utilisés pour modéliser des bus 3-états.

Ils possèdent une fonction de résolution, dont le comportement est intuitif :

 

Leurs méthodes sont les mêmes que pour un sc_signal<sc_lv<n> > et sc_signal<sc_logic>.

 

sc_fifo<T>

Comme son nom l'indique, ce canal implémente une fifo (abstraite), de longueur définie lors de son instanciation (paramètre de construction). C'est un moyen de communication point à point, autrement dit il ne peut avoir qu'un seul processus lecteur, et un seul écrivain.

Comme pour sc_signal, T peut être de n'importe quel type, pourvu qu'il dispose d'un opérateur de copie et de comparaison.

Cette fifo se comporte comme un signal : une écriture dedans n'est prise en compte qu'à la fin du delta-cycle courant. La valeur écrite ne pourra donc être lue au plus tôt qu'au delta-cycle suivant.

Elle implémente les interfaces sc_fifo_in_if et sc_fifo_out_if, déjà vues à la partie sur les interfaces.

Exemples d'utilisation :

// sc_fifo de longueur par défaut égale à 16
sc_fifo<int> fifo;
int a,b;

a = fifo.read();
b = fifo;

fifo.write(a);
fifo = b;

if(fifo.num_available() > 3) 
a = fifo;

 

sc_mutex

Ce canal implémente un mutex (un verrou d'accès concurrent). Il permet à plusieurs processus d'accéder à une ressource partagée de façon sûre : avant chaque accès, un processus est sensé essayer d'acquérir le mutex. S'il y arrive, le mutex est verrouillé, et seul ce processus-là peut le déverrouiller. Il peut alors accéder tranquillement à la ressource partagée : SystemC garantit qu'aucun autre processus ne pourra acquérir le verrou alors qu'il est verrouillé.

Si un processus essaye d'acquérir le verrou de façon bloquante (méthode lock() ) alors que le verrou est déjà pris, le processus est suspendu. Il ne sera réveillé qu'à la libération du verrou.

Attention le fait qu'un processus doit acquérir le verrou avant d'accéder à la ressource partagée est une convention. SystemC ne peut pas le vérifier.

Les mutex implémentent l'interface sc_mutex_if, vue à la partie sur les interfaces.

Exemples d'utilisation :

sc_mutex mutex; 


...

while( true ) {
wait(clock.posedge_event());
mutex.lock();
... // do something
mutex.unlock();
}

sc_semaphore

Les sémaphores sont une généralisation des mutex à n accès concurrents. Le sémaphore est initialisé à sa construction avec une valeur n. Lorsqu'un processus veut accéder à une ressource partagée, il doit d'abord accéder au sémaphore. Si le sémaphore a une valeur >0, il est décrémenté et le processus peut accéder à la ressource partagée. Sinon, le processus est mis en veille jusqu'à ce que le sémaphore redevienne > 0. Lorsqu'un processus a fini d'accéder à la ressource partagée, il relâche le sémaphore, ce qui a pour effet de l'incrémenter (et éventuellement de réveiller des processus suspendus en attente du sémaphore).

Si plusieurs processus tentent d'acquérir le sémaphore pendant le même delta_cycle, un seul y arrivera. Mais celui qui y arrivera n'est pas déterminé (car l'ordre d'exécution des processus dans un delta_cycle n'est pas déterminé).

Les sémaphores implémentent l'interface sc_semaphore_if.

Exemples d'utilisation :

// instancie un semaphore à 5 accès concurrents

sc_semaphore sem(5);


// instancie un sémaphore à 1 accès concurrent = mutex
sc_semaphore mut(1);



while( true ) {
wait(clock.posedge_event());
sem.wait();
... // do something
sem.post();
}


Exercices

int a;
sc_signal<int> b;
sc_signal<int> c;

void f() {
a = 2;
b = a;
c = b;
}

N'oubliez pas de mettre les réponses dans votre dépôt. (était-ce vraiment la peine de le mentionner ?)...

 

En résumé :

Les canaux sont les moyens de communications de SystemC, une sorte de généralisation des signaux de Verilog et VHDL.

Le type le plus utilisé, pour modéliser des fils et des bus, est le type sc_signal ou sc_rv (3-états)

Les canaux fournissent des méthodes d'accès (read, write, =) et déclenchent éventuellement des événements (accessibles par event). Les méthodes s'éxécutent dans le contexte (au sens UNIX) du processus appelant, autrement dit, si elles bloquent, c'est le thread du processus appelant qui est bloqué, pas le reste du canal.

Les méthodes implémentées par un canal sont déclarées dans une interface.

 

Passons maintenant à l'étude des ports .

 

 

Back to top

Les ports

Les ports sont les parties d'un module par lesquelles passent toutes les données qui y entrent ou en sortent. Plus précisément, c'est ce qui permet à un module d'accéder à l'interface d'un canal externe.

Un port hérite d'une interface particulière : il ne peut donc être relié qu'à un canal qui implémente la même interface. En pratique, tout ceci est caché, car on utilise souvent des ports spécialisés pour chaque type de signal.

La plupart des ports sont destinés à être connectés à des signaux. Ils possèdent donc les méthodes des interfaces propres aux signaux (read, write, event, ...). Lors de l'appel du la méthode read d'un port, tout ce que fait ce port c'est de forwarder cet appel à la méthode read du signal auquel il est relié.

 

Règles de connexion

Les ports sont soumis à des règles de connexion :

 

Les deux première règles sont des conventions SystemC. Les classes ont été écrites avec cette convention en tête, et fournissent des mécanismes pour essayer de vérifier (soit à la compilation, soit lors de l'exécution) que le programmeur les a bien respectées. Mais il n'est pas possible d'en faire une vérification totale. Il est donc possible de les transgresser, mais ce n'est vraiment pas une bonne idée !

 

Spécialisés ou pas ?

Quand on déclare un port on a le choix :

 

Port normal

C'est la solution la plus compliquée... Tant mieux, la prochaine façon de faire n'en sera que meilleure ! :)

Avant de faire une déclaration manuelle, il faut connaitre le nom de l'interface du canal qui lui sera connecté. Pour les canaux prédéfinis, les interfaces sont disponibles à la section interfaces.

Syntaxesc_port<interface_type, N> port_name, port_name,... ;

 

Pour accéder au canal qui lui est relié, on utilise l'opérateur -> .
Si un port est relié à plusieurs canaux, on utilise l'opérateur [] pour spécifier auquel on s'adresse.

Exemple de déclaration :

exemples de ports

 

Exemple d'utilisation de ces ports :

Utilisation des ports

Utilisation de ports spécialisés

Quand un port est relié à un signal ou une fifo, il existe des types de ports spécialisés, plus simples à instancier :

Type du canal Entrée Sortie Bidir
sc_signal, sc_buffer
sc_in
sc_out
sc_inout
sc_signal_rv
sc_in_rv
sc_out_rv
sc_inout_rv
sc_signal_resolved
sc_in_resolved
sc_out_resolved
sc_inout_resolved
sc_fifo
sc_fifo_in
sc_fifo_out
(pas de sens)

 

Exemple : le type sc_in<T> est équivalent à sc_port<sc_signal_in_if<T>, 1> .

On accède à ces types de port non pas par l'opérateur -> mais directement par l'opérateur "." .

Exemple :

Utilisation des ports

Remarque : les anciennes versions de SystemC définissaient trois types spéciaux pour des horloges. Ces types sont maintenant obsolètes

 

En résumé :

Les ports sont les moyens pour faire communiquer l'intérieur d'un module avec l'extérieur.

A l'extérieur du module, ils sont soit reliés à un canal, soit directement à un port du module parent.

On peut les instancier à la main, avec la classe sc_port<>, mais le moyen le plus simple est d'utiliser des classes dédiées : des ports spécialisés pour un type de canal.

Les principaux ports spécialisés sont :

  • pour un sc_signal : sc_in, sc_out et sc_inout
  • pour un sc_signal_resolved : sc_in_resolved, sc_out_resolved et sc_inout_resolved
  • pour un sc_signal_rv : sc_in_rv, sc_out_rv et sc_inout_rv

 

On accède aux fonctions d'un port normal par "->" s'il est instancié à la main.
On accède aux fonctions d'un port spécialisé par "."

 

 Maintenant qu'on connait les canaux et les ports, on peut rassembler le tout dans des modules. C'est l'objectif de la prochaine section.

Back to top

Les modules

Les modules en SystemC sont similaires aux modules de Verilog et VHDL. Ce sont les blocs structurels de base à l'intérieur desquels sont instanciés d'autres modules, des signaux, des canaux et des processus.

Les modules peuvent contenir les éléments suivants :

 

La déclaration d'un module se fait, de façon équivalente,

 

Constructeur

Comme toute classe C++, un module possède un constructeur. Le rôle du constructeur est de faire toutes les initialisations dont l'objet a besoin :

Le passage de paramètres au constructeur d'un module est typiquement la méthode qu'on utilise pour paramétrer un objet lors de son instanciation.

Le constructeur d'une classe C++ est une méthode (fonction) particulière qui porte le même nom que sa classe et n'a aucun type. On peut le déclarer de deux façons :

Déclaration manuelle (explicite)

Comme d'habitude on regarde la solution la plus compliquée en premier...

Le constructeur d'un module se déclare comme un constructeur normal, en gardant à l'esprit qu'un de ses paramètres doit être de type sc_module_name : c'est le nom du module, qui sera utilisé par SystemC pour construire une hiérarchie des modules instanciés et produire éventuellement des messages de débug compréhensibles par les humains. Ce paramètre doit être transmis au constructeur de la classe de base (sc_module).

De plus, si le module contient des processus, il faut ajouter à la déclaration du module la macro SC_HAS_PROCESS, qui définit des symboles spéciaux utilisés par le scheduler SystemC.

Exemples :

On définit un module qu'on appelle mon_module, et on écrit son constructeur à la main. On utilise ici la syntaxe C++ qui appelle le constructeur de la classe de base en lui passant le paramètre name. Remarque : ici on aurait pu utiliser la deuxième méthode (voir plus loin) !

sc_module1

On définit maintenant un autre module, qu'on appelle memoire, en écrivant toujours son constructeur à la main. Mais ici on n'a pas le choix, car on va passer un argument supplémentaire au constructeur : le nom d'un fichier pour initialiser la mémoire.

sc_module2

Déclaration par macro SC_CTOR

Si on ne passe pas d'argument supplémentaire au constructeur, on peut utiliser une macro bien pratique : SC_CTOR, qui prend en argument le nom du module.
La macro SC_CTOR se charge d'appeller SC_HAS_PROCESS.

Le premier exemple (cf ci-dessus) s'écrit alors :

sc_module3

Ce qui est quand même un peu moins verbeux.

 

Instanciation des éléments

Maintenant que le module a son constructeur, il faut instancier les variables, méthodes, sous-modules, canaux, processus, ... Le cas des processus sera vu au prochain chapitre. Occupons-nous pour l'instant des éléments structurels (canaux, sous-modules).

Comme d'habitude en C/C++, les variables (au sens large) peuvent être instanciées de façon statique ou dynamique. C'est aussi le cas en SystemC.

La déclaration statique des objets se fait comme vu aux sections précédentes. Les objets instanciés sont alors des membres (des variables) du module.
Prenons l'exemple du compteur, et rajoutons-lui un registre en sortie (qu'on a déclaré sous le nom reg8) :

sc_module4

 

On peut aussi déclarer les objets internes dynamiquement : ce seront en fait des pointeurs vers les objets correspondant. C'est alors le constructeur qui se chargera d'allouer les objets. Bien sûr il ne faudra pas oublier de libérer la mémoire à la fin de vie du module : un bon endroit pour le faire serait dans le destructeur.

Le même exemple, en dynamique (le destructeur n'apparait pas) :

sc_module5

 

Connexion des ports des sous-modules

Dernière étape à la création des modules, la connexion des ports des sous-modules.

Cela se fait en appelant la méthode "port_du_sous_module", en lui passant en argument le signal auquel il est relié. Un bon endroit pour le faire est encore le constructeur !

Exemple :

  1. pour relier le port clk d'un sous-module reg8 à un signal clock, on appelle : reg8.clk(clock);
  2. en reprenant le dernier exemple, le module reg8 est connecté comme indiqué ci-dessous :

sc_module6

 

 

En résumé :

Les modules sont les blocs structurels de base de SystemC. Ils peuvent contenir des canaux, des sous-modules, des variables, des méthodes, des processus...

On les déclare grâce à la macro SC_MODULE, qui prend en argument le nom (la classe) du module à déclarer.

On instancie les éléments internes du module comme des membres (variables) de la classe. A part les ports, ils peuvent être créés soit statiquement, soit dynamiquement (new).

Le constructeur, généralement écrit par la macro SC_CTOR, se charge d'allouer les objets dynamiques, et de relier les signaux internes aux sous-modules.

 

 

 

Ceci clôt le chapitre sur les modules. Passons à un bref résumé du chapitre avant d'attaquer la suite.

 

 

Back to top

En résumé

Vous savez donc maintenant déclarer un module, déclarer son constructeur, déclarer et instancier les éléments qui le composent (à part les processus), et les relier entre eux.

Vous connaissez aussi ses éléments structurels (canaux, ports, interface), leur rôle et leurs intercations.

Il ne reste plus qu'à voir comment écrire les processus, et vous pourrez écrire vos propres modèles SystemC. C'est l'objectif du prochain chapitre !

 


Back to top