L’esprit humain est familier de la programmation
séquentielle. Il nous est en effet naturel de
décrire un traitement sous la forme d’un
enchaînement d’opérations à
effectuer
dans un ordre pré-établi.
Le matériel est, par essence, parallèle. Dans un
circuit intégré, tous les composants
sont actifs simultanément.
De ces deux considérations on déduit que la forme la plus probable des langages fonctionnels de description de matériel comportera des aspects séquentiels et des aspects parallèles. En d’autres termes, un modèle de composant électronique sera composé de l’assemblage de plusieurs programmes séquentiels, s’exécutant en parallèle. Le concepteur partitionnera son problème en modules assez simples pour être décrits sous la forme de programmes séquentiels classiques, puis il assemblera ces modules que le simulateur exécutera en parallèle.
Le problème, car problème il y a, naît de ce que le simulateur est, en général, installé sur une machine séquentielle, capable d’exécuter une instruction à la fois, donc un programme à la fois. Il ne pourra donc pas paralléliser réellement les différents programmes. Il faut donc trouver un moyen d’émuler le parallélisme sur une machine séquentielle. Comment faire ?
L’émulation du parallélisme repose sur une idée très simple et très efficace : pour que chaque programme séquentiel, qu’on appellera désormais processus, s’exécute “comme si” le parallélisme était réel, il faut que son environnement (ses variables d’entrée) ne change pas pendant l’exécution des autres processus. Ainsi, l’ordre d’exécution des processus n’a plus d’importance et tout se passe comme s’ils s’exécutaient en parallèle.
Pour parvenir à ce résultat, il faut que les variables partagées entre processus conservent leur valeur jusqu’à ce que tous les processus aient fini leur exécution. En quelque sorte, on simule pas à pas et, à chaque pas :
Les variables partagées ou globales doivent donc être traitées d’une façon particulière. On appelle ces variables des signaux, pour les distinguer des variables classiques.
Un simulateur / langage HDL doit avoir une notion du temps, quelle que soit son échelle / unité :
Un simulateur doit donc maintenir un compteur de temps, le temps physique courant, et attribuer une date physique à chaque événement au sein de la simulation.
Pour pouvoir modéliser des portes idéales et des conducteurs parfaits (temps de propagation nuls), il faut décorréler les pas de simulation du temps physique : prenons l'exemple d'un inverseur parfait, dont le temps de propagation est nul. Si son entrée change au temps t, sa sortie changera aussi au temps t. Pourtant les deux événements (simultanés) n'auront pas lieu lors du même pas de simulation.
Autrement dit, un pas de simulation n'a pas de valeur temporelle physique intrinsèque. C'est un intervalle de temps virtuel, appelé delta, dont la durée est nulle, qui ne sert qu'à ordonner les événements "simultanés", c'est-à-dire à déterminer lequel a provoqué lequel. Pendant un delta, le temps physique ne s'écoule pas.
Se pose alors le problème suivant : quand faire avancer le temps courant, quelle est la relation entre ce compteur (temps physique courant) et les pas de simulation ?
La solution à ce problème est simple et élégante, et possède pour une fois la bonne propriété d’accélérer considérablement les simulations.
En fait, cette optimisation est même indispensable puisque la durée physique d’un delta est nulle. Les durées symboliques ont pour seule fonction d’ordonner les événements les uns par rapport aux autres. Si le simulateur ne faisait avancer le temps que d’un delta à la fois, le temps physique n’avancerait pas.
L’aspect séquentiel
est conservé à
l’intérieur des processus. Ils
obéissent aux règles très classiques
de la programmation séquentielle. On trouve les
mêmes instructions et les mêmes structures de
contrôle qu’en langage C, en Pascal
ou en Ada.
La seule exception à l’exécution
séquentielle des
processus concerne l’affectation des signaux
(voir ci-dessous).
De plus, chaque processus est une boucle infinie.
Il doit donc comporter au moins un point de synchronisation
(appelé aussi point d'arrêt).
Faute de quoi, le simulateur est dans l’obligation de
l’exécuter indéfiniment (temps physique
et temps symbolique n’évoluent pas, il
s’agit d’un processus cyclique sans point
d’arrêt).
Ce point d'arrêt est soit :
Lorsqu'un processus a atteint un point d'arrêt, le simulateur passe au processus suivant. S'ils ont tous été traités, le simulateur fait avancer le temps (symbolique ou physique, selon l'échéancier).
Attention, la condition de réveil
associée au point d'arrêt (implicite ou explicite)
ne doit pas être toujours vraie, sinon seul le temps
symbolique évolue.
Les processus cycliques sans point d’arrêt sont un
grand piège
des langages fonctionnels de description de matériel
(surtout en VHDL).
On peut considérer les processus comme des threads d'un OS multitâche, sachant que l'OS en question est non-préemptif : chaque thread doit rendre la main au scheduler. C'est le rôle des points d'arrêt.
Les variables globales (signaux)
servent à la communication entre les processus (cf.
ci-dessus). Elles modélisent les fils électriques
(même si ce n'est pas la seule façon de les
modéliser).
On a déjà vu que leur affectation doit
être traitée de façon
spéciale (déférée
à la fin du delta courant). Lors de l'affectation des
signaux, une instruction d’affectation spéciale
est créée. Lorsqu’une telle instruction
est exécutée, elle ne prend pas effet
immédiatement. Elle est
“enregistrée” par le simulateur, qui la
traitera effectivement lorsque tous les processus auront
été exécutés.
C’est un piège classique des langages fonctionnels
de description de matériel que de croire
l’affectation de signal instantanée. Le paradoxe
apparent provient du fait qu’après une telle
affectation
le signal n’a pas changé de valeur ...
Les variables locales aux processus sont traitées comme dans tout langage de programmation classique. A la différence des signaux, elles sont modifiées dès l’exécution d’une instruction d’affectation qui les concerne. Ces variables locales ne sont, en général, pas visibles de l’extérieur du processus où elles sont déclarées. Un autre processus ne peut ni les lire, ni les modifier. Elles ne sont pas réinitialisées d’une exécution à l’autre du processus ; elles conservent leur valeur, comme si le processus était en fait une boucle infinie dans un langage de programmation classique.
La différence entre variable locale et signal dépend du langage utilisé :
Le temps est décomposé en temps physique et temps symbolique. Deux événements ayant le même temps physique sont simulanés. Le temps symbolique ne sert qu'à déterminer la dépendance des événements entre eux.
La simulation procède par pas,
Les affectations de variables doivent être
instantanées si les variables sont locales à un
processus, et différés à la fin du
delta courant si ces variables sont des signaux de communication entre
processus.
La différenciation des affectation est
gérée directement par le langage en SystemC,
laissé aux soins du codeur en Verilog et en VHDL.
Chaque processus est une boucle inifinie qui doit être stoppée par un point d'arrêt implicite ou explicite, sinon le temps physique ne s'écoule pas. Ce point d'arrêt définit une liste de sensibilité. Un processus n'est alors exécuté (réveillé) que lors d'un événement portant sur un signal membre de cette liste de sensibilité.