Plutôt que de long discours, voici quelques exemples de modules écrits en SystemC. Chaque exemple introduit un nouveau type de construction / type de donnée.
Pour chaque exemple, passez la souris sur le code pour avoir des explications sur la façon dont il est construit.
Tous les modules utilisant des classes de SystemC doivent inclure cette déclaration.
Il est nécessaire de donner à gcc, lors de la compilation, le chemin vers les bibliothèques SystemC. Pour les machines de COMELEC, c'est avec l'option -I/comelec/softs/opt/systemc/systemc-2.3.0/include.
Typiquement, ce genre d'option se positionne dans un Makefile...
Comme dans l'exemple précédent, on déclare le module à l'aide de la macro SC_MODULE.
On aurait pu déclarer le module de façon plus classique, en déclarant une classe héritant de sc_module, cela sera vu dans un exemple ultérieur.
Une bascule D a deux ports en entrée (din et clock), et un port en sortie (dout).
Ces ports seront connectés à des signaux sur 1 bit. Deux conséquences :
- les ports seront connectés à des signaux, ils devront donc être spécialisés pour l'interface propre aux signaux. D'où les macros de type générique sc_in et sc_out.
- les signaux sont sur 1 bit, les deux types génériques sont donc spécialisés pour le type bool : sc_in<bool> et sc_out<bool>
Une bascule D simple n'a qu'un seul processus.
On définit donc une fonction (echantillonne) qui se charge de lire le port en entrée din et d'affecter le port en sortie dout.
Remarque : pour l'instant cette méthode n'est pas un processus !
Comme dans l'exemple précédent, dans le constructeur du module on enregistre la fonction echantillonne comme un processus.
On précise aussi la liste de sensibilité (clock), en ne gardant que les fronts montants.
Une bascule D avec reset asynchrone (actif à l'état bas) a aussi un seul processus, sensible à deux événements :
- le front montant de l'horloge
- l'état du reset.
Dans le corps du processus, selon le type d'événement qui a déclenché le processus, soit on met la sortie à 0, soit on échantillonne l'entrée.
Pour savoir quel signal a déclenché le processus (reset ou clock), on utilise la méthode .event du port clock. Cette méthode renvoie true si le signal auquel est relié le port a changé.
Afficher la réponseExercice : donnez la description en SystemC d'une bascule avec reset actif à l'état bas synchrone.
Réponse :
Dff avec reset synchrone
#include "systemc.h"
SC_MODULE(dffs)
{
sc_in<bool> clock;
sc_in<bool> reset;
sc_in<bool> din;
sc_out<bool> dout;
void do_bascule()
{
if (!reset) {
dout = false;
} else {
dout = din;
}
};
SC_CTOR(dffs)
{
SC_METHOD(do_bascule);
sensitive << clock.pos();
}
};
On implémente ici un compteur avec reset synchrone. Il a trois ports :
- clock : entrée, signal sur 1 bit
- reset : entrée, signal sur 1 bit
- out (valeur du compteur) : sortie, signal entier sur 8 bits.
On pourrait utiliser beaucoup de type pour la sortie out :
- les types les plus rapides étant les types natifs C++, un unsigned char aurait très bien convenu. Mais il aurait alors fallu gérer à la main le débordement (passage de 255 à 0).
- SystemC propose des vecteurs de bits, comme sc_bc ou sc_lv. Mais il n'y a pas d'opérateur d'addition de prévu pour ces types (pour un compteur, c'est gênant...). On utilise donc le type sc_uint<n>, qui est un entier non signé sur n bits.
Remarque importante : l'espace entre les deux crochets fermant est nécessaire en C++, sinon ça ne compile pas !
A chaque front montant de l'horloge, si reset est bas, on remet le compteur à 0, sinon on l'incrémente. Tout le travail est fait sur la variable d'état interne value.
Comme value est de type sc_uint<>, on peut utiliser les même opérateurs arithmétiques que ceux définit pour les entiers.
Pour maintenir l'état du compteur, on utilise une variable interne, value. On aurait pu lui donner le type unsigned char, sur 8 bits aussi. La simulation aurait été plus rapide. Mais pour rester cohérent, nous prenons le même type que le signal externe, soit sc_uint<8>.
L'utilisation d'une variable interne n'est pas obligatoire. On aurait pu, à chaque front montant d'horloge, aller lire out, l'incrémenter, et ressortir la nouvelle valeur. Mais out est définie en type sc_out, qui ne peut pas être lu (sc_out n'implémente pas la méthode read(), autrement dit on ne peut pas écrire truc = out; ). Il aurait alors fallu définir out de type sc_inout, ce qui est conceptuellement gênant, out étant une sortie. Ce genre de situation (où les sorties ne peuvent pas être lues) arrive aussi en VHDL, mais pas en Verilog.
Afficher la réponseExercice : modifiez le compteur pour lui ajouter une entrée load sur 1 bit, une entrée in sur 8 bits (si load est haut, le compteur doit être pré-chargé avec la valeur in), et modifiez le type de la variable interne value pour utiliser un unsigned char.
Est-ce qu'il compile correctement ? Vérifiez !
Pour information, la ligne de commande pour la compilation sur les stations Linux du département est :g++ -Wno-deprecated -I/comelec/softs/opt/systemc/systemc-2.3.0/include -c compt.cpp
Réponse :
Problème de la lecture des ports
Si vous avez écrit une ligne du style value = in, votre modèle ne compile pas correctement :
- value est de type unsigned char
- in est de type sc_in<sc_uint<8> >
- il y aurait donc deux transtypages implicites, le premier de sc_in vers sc_uint<8>, le second de sc_uint<8> vers unsigned char, ce qui est interdit en C++ (un seul transtypage à la fois).
Il faut alors utiliser la fonction explicite de lecture in.read() qui renvoie la valeur du signal associé au port (sc_uint<8>). Le transtypage peut alors être effectué.
Cet exemple est séparé en deux fichiers, le premier (shift.h) contient les déclarations, le deuxième (shift.cpp) contient l'implémentation.
Nous avons choisi ici de déclarer le constructeur du module de la façon habituelle en C++, c'est-à-dire sans utiliser la macro SC_CTOR.
Le constructeur prend alors en argument le nom du module, qui servira lors du debug, et passe ce nom au constructeur de la classe de base (sc_module).
Remarque : on ne doit pas préciser le type de renvoi du constructeur, même pas void...
La macro SC_CTOR n'étant pas utilisée, on doit utiliser la macro SC_HAS_PROCESS pour spécifier que ce module a un processus.
Dans l'en-tête du module, on déclare un tableau de variables internes regs qui serviront à implémenter le décalage.
Ils sont de type sc_signal, ce sont donc des signaux : les affectations à des signaux sont différées, ce qui permet d'effectuer les affectations (méthode shifter::shift) dans n'importe quel ordre.
Si on les avait défini de type bool, la simulation aurait été plus rapide, mais les affectations auraient alors été immédiates : il aurait fallu faire attention à leur ordre.
On aurait pu aussi utiliser les types vecteurs de bits de SystemC sc_bv<8>, qui sont particulièrement adaptés aux manipulations de bits. On aurait alors eu le code suivant :
sc_bv<8> regs;
...
regs.range(1,7) = regs.range(0,6);
regs[0] = din;
dout = regs[7];
Nous avons choisi ici de déclarer le constructeur du module de la façon habituelle en C++, c'est-à-dire sans utiliser la macro SC_CTOR.
Le constructeur prend alors en argument le nom du module, qui servira lors de la génération des chronogrammes, et passe ce nom au constructeur de la classe de base (sc_module).
Remarque : on ne doit pas préciser le type de renvoi du constructeur, même pas void...
La macro SC_CTOR n'étant pas utilisée, on doit utiliser la macro SC_HAS_PROCESS pour spécifier que ce module a un processus.
Dans l'en-tête du module, on déclare un tableau de variables internes regs qui serviront à implémenter le décalage.
Ils sont de type sc_signal, ce sont donc des signaux : les affectations à des signaux sont différées, ce qui permet d'effectuer les affectations (méthode shifter::shift) dans n'importe quel ordre.
Si on les avait défini de type bool, la simulation aurait été plus rapide, mais les affectations auraient alors été immédiates : il aurait fallu faire attention à leur ordre.
On aurait pu aussi utiliser les types vecteurs de bits de SystemC sc_bv<8>, qui sont particulièrement adaptés aux manipulations de bits. On aurait alors eu le code suivant :
sc_bv<8> regs;
...
regs.range(1,7) = regs.range(0,6);
regs[0] = din;
dout = regs[7];
...ou comment gérer des bus 3 états (et des tableaux). Cet exemple modélise un chip de SRAM asynchrone, possédant :
La direction du bus, du point de vue de la RAM, est donnée par
oen : oen actif (bas) rend la RAM maître
du bus de données.
L'écriture est effectuée si wen
est actif (bas) et que le bus de données est en entrée (oen inactif/haut).
Le bus de données de la RAM est bidirectionnel, il est donc déclaré en inout.
De plus, plusieurs RAM peuvent être branchées en parallèle. Des conflits peuvent donc potentiellement arriver si plusieurs RAM mettent des valeurs différentes sur le bus de données. Le port doit donc résoudre les conflits. On utilise pour cela le type sc_inout_rv<>.
Pour un driver 3-états en sortie seulement, on aurait utilisé le type sc_out_rv<>.
Pour un port en entrée, on aurait utilisé sc_in_rv<>.Les ports résolus ne doivent être utilisés que là où c'est nécessaire, car ils simulent plus lentement que les types non résolus.
Le contenu de la RAM est stocké dans un tableau de 256 valeurs (car le bus d'adresse est sur 8 bits).
Pour détecter les lectures de RAM à des adresses non initialisées on utilise un type de logique 4-valuée. Ainsi si on lit la RAM à une adresse à laquelle on n'a encore rien écrit, on verra sortir la valeur 'X'. SystemC propose plusieurs types de données 4-valuées :
- sc_logic : sur 1bit
- sc_lv<n> : sur n bits
Le comportement de la RAM est décrit par deux processus, un pour l'écriture (mise à jour du tableau interne), l'autre pour la lecture (mise à jour du bus de données à partir du tableau interne). On aurait très bien pu écrire la RAM avec un seul processus, mais il est intéressant ici d'avoir deux processus accédant à la même ressource (bus data) en même temps.
La RAM est asynchrone, les processus de lecture et d'écriture sont donc combinatoires. Leur liste de sensibilité est ici définie par les opérateurs de flot ( << ).
On aurait pu aussi utiliser le constructeur pour faire l'initialisation de la RAM à partir d'un fichier. Typiquement, le nom du fichier aurait été transmis au constructeur du module. On n'aurait alors pas pu utiliser la macro SC_CTOR (qui n'admet pas plus d'argument que le nom du module), il aurait fallu passer par la déclaration explicite du constructeur, comme vu dans l'exemple précédent.
Un exemple de l'intérêt de SystemC : si la RAM est destinée à stocker des images provenant d'une séquence vidéo, on aurait pu l'initialiser directement à partir d'un fichier PNG ou MPEG, en utilisant l'une des bibliothèques C déjà disponibles pour cela (libpng, libmpeg, libmpeg2, ...). Pas besoin d'écrire ses propres routines de parsing MPEG ou PNG...
Le tableau est indexé par les adresses qui sont de type sc_in<sc_int<8> >. Comme vu précédemment, on est obligé d'utiliser la fonction explicite de lecture .read() au lieu d'un simple "=" .
Cela vient du fait que C++ n'autorise qu'un seul transtypage implicite, et qu'avec un "=" il y en aurait deux :
- transtypage du signal associé au port en la valeur qu'il transporte : sc_signal<sc_int<8> > vers sc_int<8>
- transtypage de sc_int<8> vers int, car le tableau de valeurs est indexé par des entiers (int).
On doit donc utiliser la fonction .read() qui renvoie la valeur du signal (sc_int<8>) qui peut alors être implicitement être transtypée vers int.
La plupart des synthétiseurs SystemC demandent à ce les accès aux signaux se fassent explicitement par les méthodes .read() et .write(), et de réserver les affectations directes "=" aux variables.
Cela rend le code un peu plus verbeux, mais plus clair.Enfin, l'affectation d'une valeur littérale à un vecteur de bits se fait par une chaîne de caractères.
Exemple : data = "11100ZX01ZXX0XZ0";
Le sujet des environnements de simulation est vaste et sera envisagé dans le dernier chapitre. Nous vous présentons ici un environnement très simple, qui vous permettra de tester les modules ci-dessus.
L'exemple ci-dessous instancie le registre à décalage vu plus haut, lui fournit une horloge et quelques vecteurs d'entrée. Il ne vérifie pas la sortie, il se contente de tracer certains signaux, laissant au concepteur le soin de visionner les chronogrammes et de vérifier qu'ils sont corrects. Bien entendu, un système réel ne serait pas testé de cette façon !
Cet exemple vous montre :
Il se compose de trois fichiers : shift.h,
shift.cpp et test_shift.cpp (le fichier de test
proprement dit). Vous pouvez récupérer l'archive complète ici.
Il produit un fichier de chronogrammes, shift_trace.vcd
qui peut être visionné à l'aide de gtkwave
ou Modelsim.
Mettez d'abord en place une variable d'environnement SYSTEMC qui indique où se trouve la bibliothèque : export SYSTEMC=/comelec/softs/opt/systemc/systemc-2.3.0
L'exemple est suffisament simple pour se passer de Makefile. La ligne de compilation :
g++ -Wno-deprecated -L$SYSTEMC/lib-linux64 -I$SYSTEMC/include test_shift.cpp shift.cpp -lsystemc -o test_shift
Une compilation séparée aurait été faite ainsi :
Le Makefile fourni avec l'archive fait exactement la même chose. Pour l'utiliser : taper make.
La compilation a généré un exécutable contenant le moteur de simulation. Il reste à lancer la simulation proprement dite :
Exercice :
La fonction main() est déjà définie par la bibliothèque SystemC. Elle appelle la fonction sc_main, fournie par l'utilisateur, qui est le point d'entrée du code utilisateur.
sc_main a le même prototype que main : int sc_main(int argc, char *argv[]);
Cette fonction a pour rôle d'instancier tous les modules et signaux nécessaires, et de lancer la simulation.
Le registre à décalage possède trois ports, trois signaux sont donc nécessaires à son test.
On instancie donc trois signaux, de type bool (1 bit, en logique 2 valuée).L'horloge a aussi un type sc_signal<bool>. C'est nous qui nous chargerons de la faire bouger.
Il existe en SystemC une façon plus propre de générer des horloges, en passant par le type sc_clock.
C'est le moteur SystemC qui se charge alors de générer les transitions des objets sc_clock.
SystemC dispose de fonction de traçage de signaux au format VCD (Value Change Dump, visible par gtkwave, ... ).
SystemC permet de tracer les variables scalaires, les vecteurs de bits, les signaux et certains ports. Pour les objets ne disposant pas de fonctions de traçage (structures, objets, ...) il est toujours possible de définir ses propres fonctions de traçage, qui consistent généralement à tracer les membres de l'objet. Seuls les objets existant pendant toute la simulation peuvent être tracés. En d'autres termes, les variables locales ne sont pas traçables.
Avant de tracer un objet, il faut ouvrir un fichier de traces. C'est le but de la fonction sc_create_vcd_trace_file().
Puis on enregistre tous les objets à tracer à l'aide la fonction sc_trace, prenant en argument une référence ou un pointeur vers l'objet à tracer et un nom associé qui sera utilisé dans la génération des chronogrammes.Il est impératif, sous peine de crash, de fermer le fichier de trace avant de retourner de sc_main().
Plusieurs fichiers de traces peuvent être générés en même temps, et un même signal peut être tracé dans plusieurs fichiers à la fois.
L'instanciation d'un module se fait comme l'instanciation de n'importe quel objet C++.
On peut le créer de façon statique comme ici (il prend comme argument celui attendu par son constructeur, c'est-à-dire un nom), ou de façon dynamique à l'aide de new (new est l'équivalent de malloc en C++, il permet d'allouer dynamiquement un objet en passant des arguments à son constructeur).La connexion est faite ici de façon explicite : chaque port et relié explicitement à un signal : shifter.din(in);
Il est aussi possible, comme en Verilog ou VHDL, d'utiliser une connexion par position (implicite).
Une simulation se lance généralement par sc_start, et s'arrête par sc_stop. Comme aucun signal n'est géré automatiquement dans ce système-ci (pas de sc_clock), il faut gérer la simulation manuellement.
La simulation peut être initialisée par l'appel de la fonction sc_start(SC_ZERO_TIME), qui se charge alors de faire toutes les initialisations du scheduler SystemC, et d'exécuter une fois tous les processus qui ne sont pas marqués dont_initialize().
Mais cet appel n'est pas obligatoire : il est fait automatiquement pas le scheduler lors du lancement de la simulation s'il n'a pas déjà été fait.Remarque : le fonction sc_initialize() est obsolète, et remplacée par sc_start(SC_ZERO_TIME).
Les signaux sont alors positionnés à la main, par une simple affectation (in = 0).
Puis le temps est avancé manuellement par la fonction qu'on a appelée next_cycle(), dont le code se trouve en bas du listing : cette fonction positionne manuellement la ligne d'horloge, et fait à chaque fois avancer la simulation d'une unité de temps (1ns) grâce à la fonction sc_start().
Exercice :
Vous avez vu ici quelques exemples de modules en SystemC, et comment instancier un module et lui fournir rapidement quelques vecteurs de test.
L'objectif des prochains chapitres est d'étudier plus précisément les différents types de données disponibles, la façon de décrire structurellement un système (module, signaux, ports), puis fonctionnellement (processus, événements).
Inclusion des définitions SystemC
#include "systemc.h"Déclaration du module
SC_MODULE(and3)Déclaration des ports
Déclaration des processus
Déclaration du constructeur et des listes de sensibilité des processus