Introduction à la modélisation SystemC
Introduction à la modélisation SystemC
par J. Bhasker
(Traduction de F. Pêcheux, Septembre 2004)
1. Avertissement
2. Les bases
Un module est l’entité de base pour décrire un système en SystemC. Un module peut avoir un
nombre quelconque d’entrées, de sorties, ou d’entrées/sorties. Un module peut contenir un
nombre quelconque de processus. Un processus sert à décrire la fonctionnalité d’un système,
et permet d’émuler le comportement concurrent de ses composants. Chaque processus est dit
sensible à un ensemble de signaux et de ports, et s’exécute dès qu’il y a une modification sur
un des signaux ou ports de la liste de sensibilité. Les signaux sont utilisés pour les
communications inter-processus. De plus, l’affectation d’une valeur à un signal (ou à un port)
se fait toujours avec un délai « delta » ; un délai « delta » est un délai infiniment court servant
à modéliser les relations de cause à effet de la logique électronique. Un module permet aussi
de décrire la hiérarchie, ce qui permet d’instancier un module dans un autre, comme indiqué à
la figure 2-1.
La figure 2-2 montre le circuit d'un demi-additionneur classique, qui va servir d'exemple tout
au long du chapitre.
Figure 2-2 : le demi-additionneur.
1. // File : half_adder.h
2. #include "systemc.h"
3.
4. SC_MODULE(half_adder)
5. {
6. sc_in<bool> a,b;
7. sc_out<bool> sum,carry;
8.
9. void prc_half_adder();
10.
11. SC_CTOR(half_adder)
12. {
13. SC_METHOD(prc_half_adder);
14. sensitive << a << b;
15. }
16.};
1. // File : half_adder.cpp
2. #include "half_adder.h"
3.
4. void half_adder::prc_half_adder()
5. {
6. sum = a ^ b;
7. carry = a & b;
8. }
Le bloc SC_CTOR à la ligne 11 déclare les processus, et types de processus, utilisés pour
décrire le comportement du module. Dans le module half_adder, le bloc SC_CTOR
declare un processus de type SC_METHOD. Le nom associé au bloc SC_CTOR doit être
identique à celui du module. Un processus de type SC_METHOD est sensible à un ensemble
fini de signaux et de ports et ne peut être temporairement arrêté à l’aide d’une instruction
wait ; les instructions wait , qui permettent d'arrêter un processus en plain milieu de son
exécution, ne sont pas autorisées à l’intérieur d’un processus SC_METHOD. L’autre type de
processus est le processus SC_THREAD, abordé plus loin.
Le nom d’un processus est défini dans la déclaration du processus SC_METHOD. Dans notre
cas, le nom du processus est prc_half_adder, déclaré à la ligne 9. Le processus, qui du
point de vue C++ est une fonction membre, doit retourner void, et ne doit pas prendre
d’arguments. L’instruction sensitive qui apparaît ligne 14 sert à spécifier les signaux et ports
auxquels le processus SC_METHOD est sensible. Ici, le processus prc_half_adder est
sensible aux deux ports d’entrée a et b. Cela signifie que toute modification de valeur sur le
port a ou sur le port b entraîne l’exécution complète de la fonction membre
prc_half_adder. Une fois que cette fonction a été exécutée, le processus
prc_half_adder se met en attente d’autres événements sur a ou b. Notez que la
déclaration d’un processus SC_METHOD se termine par un point-virgule.
3. Un autre exemple
Voyons un autre exemple, celui d’un décodeur 2 vers 4, montré à la figure 2-3.
De même, le port de sortie est de type sc_uint<4>, soit donc un vecteur entier sur 4 bits.
sc_uint est un type SystemC prédéfini. Notez qu’un caractère espace doit être inséré entre
la taille <2> et le caractère > de fin de type, pour que le modèle soit compilable sans erreur.
1. // File : decoder2by4.h
2. #include "systemc.h"
3.
4. SC_MODULE(decoder2by4)
5. {
6. sc_in<bool> enable;
7. sc_in<sc_uint<2> > select;
8. sc_out<sc_uint<4> > z;
9.
10. void prc_decoder2by4();
11.
12. SC_CTOR(decoder2by4)
13. {
14. SC_METHOD(prc_decoder2by4);
15. sensitive (enable,select);
16. }
17.};
1. // File : [Link]
2. #include "decoder2by4.h"
3.
4. void decoder2by4::prc_decoder2by4()
5. {
6. if (enable)
7. {
8. switch ([Link]())
9. {
10. case 0: z=0xE ; break;
11. case 1: z=0xD ; break;
12. case 2: z=0xB ; break;
13. case 3: z=0x7 ; break;
14. }
15. }
16. else
17. z=0xF;
18.}
Listing 2-4 : le fichier [Link].
Le bloc constructeur, commençant par SC_CTOR, déclare un processus SC_METHOD nommé
prc_decoder2by4. Ce processus est sensible aux ports d'entrée select et enable.
Notez que la liste de sensibilité pour ce processus SC_METHOD est décrite ici avec un style
différent, sous forme d’un appel de fonction.
sensitive(enable,select) ;
Dans le module half_adder décrit dans la section précédente, nous avons utilisé le style
“notation par flux”. Si on souhaitait faire de même ici, il faudrait écrire :
Quel que soit le style de notation utilisé, il est possible d’ajouter chaque élément à la liste de
sensibilité de manière indépendante :
sensitive(select) ;
sensitive(enable) ;
4. Description de la hiérarchie
La hiérarchie d’un système peut être aussi obtenue par le biais d’un SC_MODULE. Voici, à la
figure 2-4, un exemple d’additionneur complet, qui instancie deux fois le demi-additionneur
vu précédemment.
Le module full_adder dispose de trois ports d’entrée et de deux ports de sortie, tous de
type bool. Notez aussi dans le listing la présence d’une directive d’inclusion du fichier
half_adder.h, qui est nécessaire pour compiler correctement le full_adder. Il n’est
pas utile ici d’inclure systemc.h, puisque c’est déjà fait dans half_adder.h.
1. // File : full_adder.h
2. #include "half_adder.h"
3.
4. SC_MODULE(full_adder)
5. {
6. sc_in<bool> a,b,carry_in;
7. sc_out<bool> sum,carry_out;
8.
9. sc_signal<bool> c1,s1,c2;
10. void prc_or();
11.
12. half_adder *ha1_ptr,*ha2_ptr;
13.
14. SC_CTOR(full_adder)
15. {
16. ha1_ptr=new half_adder("ha1");
17. // Named association:
18. ha1_ptr>a(a);
19. ha1_ptr>b(b);
20. ha1_ptr>sum(s1);
21. ha1_ptr>carry(c1);
22.
23. ha2_ptr=new half_adder("ha2");
24. // Named association:
25. ha2_ptr>a(s1);
26. ha2_ptr>b(carry_in);
27. ha2_ptr>sum(sum);
28. ha2_ptr>carry(c2);
29. // Positional association:
30. //(*ha2_ptr)(s1,carry_in,sum,c2);
31.
32. SC_METHOD(prc_or);
33. sensitive << c1 << c2;
34. }
35.
36. // A destructor
37. ~full_adder()
38. {
39. delete ha1_ptr;
40. delete ha2_ptr;
41. }
42.};
1. // File : full_adder.cpp
2. #include "full_adder.h"
3.
4. void full_adder::prc_or()
5. {
6. carry_out = c1 | c2;
7. }
déclare des signaux locaux dans le module full_adder, de type bool. Les signaux sont
utilisés comme médium de communication entre les processus et les instances de modules. La
ligne :
half_adder *ha1_ptr,*ha2_ptr;
crée une nouvelle instance de module half_adder avec pour nom "ha1". Le pointeur
retourné est affecté à la variable ha1_ptr, et est utilisé pour connecter les ports et signaux
externes aux ports de l’instance. Cette interconnexion peut se faire de deux manières :
• l’association positionnelle,
• l’association nommée.
Pour l’instance ha1, nous utilisons l’association nommée. Le port a de l’instance ha1 est
connecté au port a du module full_adder. Le port b de ha1 est connecté au port b du
module full_adder. Le port sum de ha1 est connecté au signal s1, tandis que le port
carry est connecté au signal c1.
Le bloc SC_CTOR déclare aussi un processus SC_METHOD nommé prc_or, qui est sensible
aux signaux c1 et c2. Le comportement de ce processus est décrit dans full_adder.cpp.
Ce processus réalise le ou logique entre les deux retenues intermédiaires et sert à générer la
sortie carry_out.
Ce module dispose d’un destructeur. Le destructeur supprime de la mémoire ce qui a été créé
avec l’opérateur new dans le constructeur SC_CTOR. Il est important de faire cela pour éviter
les fuites mémoire. Pour les utilisateurs connaissant peu le C++, voici à quoi ressemble un
destructeur :
~module_name()
{
delete ptr1 ;
delete ptr2 ;
…
// ptr1 et ptr2 sont des pointeurs correspondant à des zones allouées
dans le SC_CTOR
}
Notez que le destructeur n’est utile que lorsque de la mémoire est allouée dans un bloc
SC_CTOR.
5. Vérification de la fonctionnalité
Une fois que l’on a écrit un modèle SystemC, comment tester sa fonctionnalité ? SystemC
propose un environnement de simulation et un ensemble de fonctions pour faire cela. Nous
allons maintenant aborder la génération d’une horloge interne, et la capture des traces de
simulation.
1. // File : driver.h
2. #include "systemc.h"
3.
4. SC_MODULE(driver)
5. {
6. sc_out<bool> d_a,d_b,d_cin;
7.
8. void prc_driver();
9.
10. SC_CTOR(driver)
11. {
12. SC_THREAD(prc_driver);
13. }
14.};
1. // File : [Link]
2. #include "driver.h"
3.
4. void driver::prc_driver()
5. {
6. sc_uint<3> pattern;
7. pattern=0;
8.
9. while (1)
10. {
11. d_a=pattern[0];
12. d_b=pattern[1];
13. d_cin=pattern[2];
14. wait(5,SC_NS);
15. pattern++;
16. }
17.}
1. // File : monitor.h
2. #include "systemc.h"
3.
4. SC_MODULE(monitor)
5. {
6. sc_in<bool> m_a,m_b,m_cin,m_sum,m_cout;
7.
8. void prc_monitor();
9.
10. SC_CTOR(monitor)
11. {
12. SC_METHOD(prc_monitor);
13. sensitive << m_a << m_b << m_cin << m_sum << m_cout;
14. }
15.};
1. // File : [Link]
2. #include "monitor.h"
3.
4. void monitor::prc_monitor()
5. {
6. cout << "At time " << sc_time_stamp() << "::";
7. cout << "(a, b, carry_in): ";
8. cout << m_a << m_b << m_cin;
9. cout << " (sum, carry_out): " << m_sum << m_cout << endl;
10.}
1. // File : full_adder_main.cpp
2. #include "driver.h"
3. #include "monitor.h"
4. #include "full_adder.h"
5.
6. int sc_main(int argc,char *argv[])
7. {
8. sc_signal<bool> t_a, t_b, t_cin, t_sum, t_cout;
9.
10. full_adder f1("FullAdderWithHalfAdder");
11. // Connect using positional association:
12. f1 << t_a << t_b << t_cin << t_sum << t_cout;
13.
14. driver d1("GenerateWaveforms");
15. // Connect using named association:
16. d1.d_a(t_a);
17. d1.d_b(t_b);
18. d1.d_cin(t_cin);
19.
20. monitor mo1("MonitorWaveforms");
21. mo1 << t_a << t_b << t_cin << t_sum << t_cout;
22.
23. sc_start(100,SC_NS);
24.
25. return(0);
26.}
Pour simuler un modèle SystemC, nous devons tout d’abord écrire une fonction sc_main()
dans le fichier full_adder_main.cpp, le programme principal. Cette fonction prend
deux arguments en paramètre : argc, le nombre d’arguments de la ligne de commande, et
argv, un tableau contenant ces arguments.
Examinons tout d’abord le module driver. Ce module dispose de trois ports de sortie, et
d’aucun port d’entrée. Le bloc SC_CTOR de module déclare un processus de type
SC_THREAD. Un tel processus peut être interrompu par des instructions wait. Une
instruction wait peut attendre pendant un certain temps, ou attendre qu’une certaine
condition sur des signaux ou des ports se réalise, ou toute autre combinaison. Le processus
SC_THREAD définit une variable locale pattern, de type entier sur 3 bits. La boucle
while permet les itérations et sert à affecter chaque pattern aux ports de sortie. A chaque
tour de boucle, pattern est incrémenté. pattern est un exemple de variable, qui s’oppose
à la notion de signal ou de port. Une variable est affectée à temps nul (pas de délai delta). En
d’autres termes, une affectation pour une variable se produit instantanément.
SystemC autorise la sélection de bit dans une variable de type sc_uint, grâce à l’opérateur
[]. Ainsi, pattern[0] représente le bit 0 de l’entier non signé. Les trois affectations ont
pour effet d’affecter le bit 0 du pattern au port d_a, le bit 1 au port d_b, et le bit 2 au port
d_cin. L’instruction wait, dernière instruction de la boucle, permet au processus de «
s’endormir » pendant 5 ns. SC_NS est l’unité de temps et représente des nanosecondes.
Le module monitor n’a que des ports d’entrée. Il suit les évolutions de toutes les entrées et
sorties de l’instance de module full_adder. Ce suivi est fait au moyen d’un processus
SC_METHOD qui est appelé à chaque modification d’un de ses ports d’entrée. La méthode
prc_monitor est appelée pour afficher toutes ces valeurs. La fonction prédéfinie
sc_time_stamp() retourne la valeur courante de l’horloge de simulation.
C’est dans la fonction sc_main() que tous les composants driver, full_adder et
monitor sont interconnectés. Notez qu’au début de ce fichier, trois directives d’inclusion
permettent au fichier full_adder_main.cpp de connaître ces modules.
L’instruction
sc_start(100, SC_NS) ;
Notez qu’il y a plusieurs lignes avec la même valeur de temps. Ce qui est tout à fait normal
puisque l’on appelle le monitor à chaque variation sur une entrée ou une sortie. Des telles
variations peuvent survenir après un ou plusieurs délais delta pour un temps de simulation
donné. Si vous souhaitez n’afficher des valeurs que lorsque les ports de sortie varient, il faut
réduire la liste de sensibilité dans le processus monitor à :
Comme tout projet C++, la compilation d'un exécutable de simualtion SystemC est
grandement facilitée par l'usage d'un fichier Makefile. Les exemples donnés avec la
distribution SystemC 2.0.1 s'appuient sur deux fichiers [Link] et
[Link]. Pour compiler le projet, il suffit de faire :
make f [Link]
Dans le cas où vous utilisez une autre distribution de SystemC que celle installée dans /
users/outil/m1archi/systemc-2.0.1, il faut modifier la ligne 2 du fichier
[Link]. Pour adapter le fichier [Link] à vos projets, il suffit de
modifier le contenu de sa ligne 11.
1. TARGET_ARCH = linux
2.
3. CC = g++
4. OPT = O3
5. DEBUG = g
6. OTHER = Wall Wnodeprecated
7. CFLAGS = $(OTHER) $(DEBUG)
8. # CFLAGS = $(DEBUG) $(OTHER)
9.
[Link] = run
[Link] = [Link] full_adder.cpp full_adder_main.cpp half_adder.cpp [Link]
[Link] = $(SRCS:.cpp=.o)
13.
[Link] ./[Link]
Etape 3 : Une fois que le simulateur est averti de la liste des signaux à tracer, il faut lancer la
boucle de simulation proprement dite, à l'aide des fonctions sc_start() ou sc_cycle()
(auquel cas il faut avoir fait un sc_initialize() avant). A chaque fois qu'un signal
change de valeur, cette modification est propagée dans le fichier de traces.
sc_initialize();
sc_trace_file *tfile=sc_create_vcd_trace_file("results");
sc_trace(tfile,t_a,"t_a") ;
sc_trace(tfile,t_b,"t_b") ;
sc_trace(tfile,t_cin,"t_cin") ;
sc_trace(tfile,t_sum,"t_sum") ;
sc_trace(tfile,t_cout,"t_cout") ;
sc_close_vcd_trace_file(tfile);
return(0);
Listing 2-14 : Pour générer un fichier de traces.