Les canaux de communication
2023-2024
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
{
<< "Hi" << endl;
cout }
};
class CplxImpl : virtual public MyInterface {
public:
virtual void hello() override
{
<< "Hi " << message << endl;
cout }
(string s): message(s) {}
CplxImpl
private:
const string message;
};
int main()
{
// MyInterface x; // ceci est une erreur car la classe est abstraite
* o[2];
MyInterface
[0] = new SimpleImpl();
o[1] = new CplxImpl("folks");
o
for (int i=0; i<2; i++)
[i]->hello();
o
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
.sc_prim_channel
sc_port
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 :
wait()
…)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).
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
{
<8> tag;
sc_uint<8> cur_d, new_d;
sc_uintm_ev;
sc_event
public:
(tagged_bus) {
SC_CTOR= 0;
tag = 0;
cur_d = 0;
new_d }
// dans tag_write_if
virtual void write(sc_uint<8> i) {
= i;
new_d // 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) {
= new_d;
cur_d ++;
tagm_ev.notify(SC_ZERO_TIME);
}
}
};
(W) {
SC_MODULE// un port en sortie n'implémente que l'interface d'écriture
<tag_write_if> p_o;
sc_port
(W) {
SC_CTOR(loop);
SC_THREAD}
void loop () {
<8> v = 1;
sc_uintwhile(v) {
// on appelle la méthode write de l'interface
->write(v);
p_o= v<<1;
v (10, SC_NS);
wait}
}
};
(R) {
SC_MODULE// un port en entrée n'implémente que l'interface de lecture
<tag_read_if> p_i;
sc_port
(R) {
SC_CTOR(loop);
SC_METHOD// Utilise le default_event
<< p_i;
sensitive }
void loop () {
// on appelle la méthode read de l'interface
<16> t = p_i->read();
sc_uint<< name() <<" --> tag: " << t(15,8) << " val: " << sc_bv<8>(t(7,0))<< endl;
cout }
};
int sc_main(int argc, char * argv[])
{
("t_bus");
tagged_bus b
("writer");
W w.p_o(b);
w
("reader");
R r.p_i(b);
r
();
sc_start
return 0;
}
sc_signal
Les sc_signal
utilisent ces mêmes notions d’interfaces
et canaux et de ports.
Pour les signaux les deux interfaces suivantes sont définies :
sc_signal_in_if<T>
: interface en lecturesc_signal_inout_if<T>
: interface en
écritureCes deux interfaces sont définies comme des classes template, où le
template T
est le type des données transporté.
sc_signal_in_if<T>
: définit la méthode
read()
ainsi que les méthodes renvoyant les
évènements.sc_signal_inout_if<T>
: la complète en ajoutant
entre autres la méthode write()
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.
sc_in
,
sc_out
sc_in
est équivalent à un
sc_port<sc_signal_in_if<T>,1>
sc_out
et un sc_inout
sont équivalents
à un sc_port<sc_signal_inout_if<T>,1>
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.
sc_signal
Est défini comme :
template <class T,
= SC_ONE_WRITER>
sc_writer_policy WRITER_POLICY class sc_signal
: public sc_signal_inout_if<T>, public sc_prim_channel
{ ... };
T
est le type transportéWRITER_POLICY
détermine le comportement du
sc_signal
s’il est modifié par plusieurs processus. Par
défaut, c’est interdit.En plus des sc_signal
, dans la bibliothèque, un certain
nombre de canaux standards sont définis.
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
sc_fifo<T>
Permet d’instances des fifos dont la taille est définie à l’instanciation.
Elles implémentent les interfaces :
sc_fifo_in_if<T>
sc_fifo_out_if<T>
Des ports spéciaux sont aussi prévus :
sc_fifo_in
sc_fifo_out
L’interface sc_fifo_in_if<T>
fournit :
bool nb_read(&T)
: lecture non bloquante qui
renvoie true
si elle réussit.void read(&T)
et T read()
: lecture
bloquante (appelle wait()
si la fifo est vide).int num_available()
: qui renvoie le nombre d’éléments
disponibles dans la fifo.sc_event& data_written_event()
: renvoie une
référence vers évènement notifié en cas d’écriture.L’interface sc_fifo_out_if<T>
fournit :
bool nb_write(&T)
: écriture non bloquante qui
renvoie true
si elle réussit.void write(&T)
: écriture bloquante (appelle
wait()
si la fifo est pleine)int num_free()
: renvoie le nombre d’éléments pouvant
être écrits dans la fifo.sc_event & data_read_event()
: une référence vers
un évènement notifié en cas de lecture.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
<int> A;
sc_fifo// une fifo de 32 éléments
<int> B(32);
sc_fifo
// deux écritures bloquantes
.write(1);
A= 1;
B
// deux lectures bloquantes
int x;
= A;
x = B.read(); x
Exemple plus complet :
#include <systemc.h>
(A) {
SC_MODULE<int> out;
sc_fifo_outvoid loop() {
int i = 0;
for(;;){
// write est obligatoire
.write(i);
out++;
i(33, SC_NS);
wait}
}
(A) { SC_THREAD(loop); }
SC_CTOR};
(B) {
SC_MODULE<int> in;
sc_fifo_invoid loop() {
(300, SC_NS);
waitfor(;;){
// in.read() appelle in->read()
<< "Lecture 1 : " << in->read() << "@ " << sc_time_stamp() << endl;
cout << "Lecture 2 : " << in.read() << "@ " << sc_time_stamp() << endl;
cout (55, SC_NS);
wait}
}
(B) { SC_THREAD(loop); }
SC_CTOR};
int sc_main(int, char **) {
// une fifo de 10 entiers
<int> fifo(10);
sc_fifo
("modA");
A a.out(fifo);
a
("modB");
B b.in(fifo);
b
(300,SC_NS);
sc_start
cout << "contenu de la fifo @" << sc_time_stamp() << endl
<< fifo << endl;
(1,SC_US);
sc_start
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
.
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 :
int lock()
: pour verrouiller le mutex. S’il est déjà
verrouillé, on est mis en attente (wait()
est appelée).
Cette méthode renvoie toujours 0.int trylock()
: pour essayer de verrouiller le mutex.
Cette méthode renvoie 0 en cas de succès,
-1 sinon.int unlock()
: pour libérer le mutex. Cette méthode
renvoie 0 en cas de succès, -1 sinon
(i.e. le mutex n’est pas verrouillé ou il appartient à un autre
thread).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 :
int wait()
: si le nombre de sémaphores restants est
supérieur à 0, le décrémenter et retourner, sinon, le processus est
suspendu (wait()
) Cette méthode renvoie toujours
0.int trywait()
: si le nombre de sémaphores restants est
supérieur à 0, le décrémenter et retourner 0, sinon,
retourne -1.int post()
: incrémente le nombre de sémaphores. Cette
méthode renvoie toujours 0.int get_value()
: renvoie le nombre de sémaphores
disponibles.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
.
© Copyright 2016-2024, Tarik Graba, Télécom Paris. | |
Le contenu de cette page est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 4.0 International . | |
Ce document reprend des parties du cours en ligne sur SystemC d'Alexis Polti disponible ici. |