L'objectif de ce TD est de mettre au point une représentation efficace de signaux booléens, puis de l'utiliser afin de simuler des circuits logiques simples, tels qu'on peut en trouver dans des microprocesseurs d'ordinateurs.
Dans la suite, nous travaillons avec le modèle suivant :
Une solution simple serait d'utiliser des tableaux de booléens : dans cette représentation, la cellule d'indice t contient la valeur du signal pendant le cycle t. Cela nous donne le type boolean[].
Néanmoins, cette représentation est peu compacte pour les signaux dont la valeur ne change pas souvent. Pour remédier à cela, nous pouvons utiliser une représentation dite «creuse», à base de listes, où chaque maillon contient :
Le type Signal correspond donc à l'enregistrement suivant :
class Signal { int t; boolean b; Signal next; Signal( int t_0, boolean b_0, Signal next_0 ){ t = t_0; b = b_0; next = next_0; } }
Afin de faciliter les dépôts, on placera l'ensemble du code dans un fichier signal.java. Vous devez y copier l'enregistrement Signal ci-dessus (avec le constructeur). Vous placerez ensuite toutes les fonctions à implémenter dans un programme introduit par class Simu. Ces consignes doivent être suivies IMPÉRATIVEMENT.
La représentation du signal s0 ci-dessus est donnée par
Signal s0 = new Signal(0,false,new Signal(1,true,new Signal(4,false,null)));
Créer dans la fonction main de la classe Simu les signaux s1 et s2 représentés sur les figures suivantes :
Afin de pouvoir inspecter les signaux retournés par vos programmes et rechercher d'éventuelles erreurs, écrire une fonction d'affichage static void displayDebug(Signal s), suivant le format ci-dessous (ce résultat correspond à s0) :
0 => false 1 => true 4 => falseOn testera cette fonction sur les signaux s0, s1 et s2 créés à la question 2.
Ajouter à l'enregistrement Signal un second constructeur prenant une valeur booléenne b_0 et construisant le signal constant, dont la valeur à tout instant t ≥ 0 est égale à b_0. On vérifiera que les signaux ainsi engendrés sont corrects, à l'aide de la fonction displayDebug.
L'affichage précédent est parfait pour bien visualiser les données manipulées dans votre programme, mais dans certains cas, un affichage plus compact sera plus lisible. Écrire une fonction static void display(Signal s) qui affichera le signal s sur une ligne, sous la forme d'une suite de 0 et de 1, avec un chiffre par cycle. On terminera la ligne par "..." dans la mesure où la dernière valeur affichée se répète ensuite à l'infini. Ainsi display(s0) affichera :
01110...
Écrire une fonction static boolean checkSignal(Signal s), qui prend comme argument un signal et retourne true si la liste utilisée pour le représenter vérifie les propriétés suivantes (et false sinon) :
Signal incorrect_signal_0 = new Signal(0,true,new Signal(4,false,new Signal(2,true,null))); Signal incorrect_signal_1 = new Signal(2,true,new Signal(4,false,null));N'hésitez pas à utiliser cette fonction par la suite pour vérifier que les résultats de vos programmes sont raisonnables, en effectuant des tests.
Écrire une fonction static Signal arrayToSignal(boolean[] tab), qui prend en argument un tableau, que l'on supposera de longueur non nulle, et construit la représentation optimale (c'est-à-dire la plus courte possible) du signal dont la valeur au cycle i est donnée dans la cellule d'indice i du tableau, (on fait l'hypothèse que la valeur du signal pour les cycles d'indices plus grands que la taille du tableau est égale à la dernière valeur dans le tableau). Par exemple, le code boolean [] t0={true,false,false,true}; displayDebug(arrayToSignal(t0)) affichera :
0 => true 1 => false 3 => trueConseil : on pourra partir de la fin du tableau.
Nous pouvons à présent passer à la programmation de quelques fonctions
simples sur les signaux.
Important :
dans les fonctions suivantes, à chaque fois que l'on calculera un
Signal à partir d'un autre, on ne modifiera pas le signal passé en
argument (de manière à pouvoir encore l'utiliser par la suite).
Écrire une fonction static boolean getVal(Signal s,int cycle)
qui prend en argument un signal et un cycle et renvoie la valeur du signal
pendant ce cycle.
Par exemple, getVal(s0,2) retournera true tandis que
getVal(s0,5) retournera false.
Écrire une fonction static Signal simplify(Signal s)
qui prend un signal en argument et retourne un nouveau signal correspondant
à sa représentation optimale.
Par exemple, si on prend la représentation non optimale Signal s3 = new
Signal(0,true,new Signal(2,true,new Signal(6,false,null))),
alors displayDebug(simplify(s3)) affichera :
0 => true 6 => falseImportant :
L'opérateur Not associe à un signal sa négation point par point.
Écrire une fonction static Signal opNot(Signal s) qui
implémente cet opérateur.
La fonction opNot devra retourner un nouveau Signal
(celui passé en argument ne sera pas modifié).
On pourra tester cette fonction sur s0, s1, s2.
L'opérateur mathématique Delay associe à un signal s le signal Delay(s) défini par :
Écrire une fonction static Signal opDelay(Signal s) qui implémente cet opérateur. La tester sur s0, s1, s2.
L'opérateur binaire And retourne le «ET» booléen point
par point de deux signaux.
Écrire une fonction static Signal opAnd(Signal s0, Signal s1)
qui implémente cet opérateur.
La tester sur s0, s1, s2.
On choisira de préférence un algorithme ayant un coût au plus linéaire en la
somme des tailles des deux listes passées en arguments.
Conseil :
cet algorithme n'est pas trivial, car il faut parcourir les deux listes
s0 et s1 en même temps ;
il faut donc bien faire attention à l'ensemble des cas pouvant se présenter
à un stade de ce double parcours (y compris les cas où on a atteint la fin
d'une des deux listes, ainsi que les ordres possibles des
indices de cycles correspondant aux premiers des éléments des deux
listes restant à traiter, lorsque celles-ci sont non vides).
Si vous êtes bloqués :
vous pouvez convertir les deux signaux en tableaux, calculer le tableau
résultat, puis repasser à un signal à l'aide de arrayToSignal
(cette solution sera moins appréciée qu'une solution écrite
entièrement sur les listes, dans la mesure où un tel algorithme aura un
coût plus que linéaire en la somme des longueurs des deux listes passées
en arguments).
Écrire une fonction static Signal opOr(Signal s0, Signal s1)
qui implémente l'opérateur Or, c'est à dire le «OU»
logique de deux signaux, point par point.
On utilisera l'identité de De Morgan, et les fonctions opAnd
et opNot, puisque X Or Y =
Not(Not(X) And Not(Y))
Nous allons à présent utiliser les opérateurs que nous avons définis afin de construire des fonctions logiques un peu plus compliquées.
En composant les fonctions élémentaires que nous avons définies,
nous pouvons aussi concevoir et simuler un circuit additionneur.
Nous considérons ici que true vaut 1 et false
vaut 0.
Un additionneur sur 1 bit prend deux entrées s0 et
s1 et calcule deux sorties sum0 et sum1
telles que, pour tout intant t,
s0(t) + s1(t) =
2 sum1(t) + sum0(t).
Définir et programmer une fonction
static Signal[] sum1(Signal s0,Signal s1) et qui retourne
un tableau contenant les deux signaux sum0 et sum1.
Généraliser le principe pour un additionneur sur n bits,
et mettre au point une fonction
static Signal[] addition(Signal[] num0,Signal[] num1)
qui calcule la somme de deux nombres positifs sur n bits
(on supposera alors que les deux tableaux passés en arguments
sont de longueur n le résultat sera alors de longueur
n+1).
L'opérateur de décalage permet de définir des signaux intéressants de
manière récursive.
Par exemple, le signal X défini par
X = Delay(Not(X)) définit
un signal alternatif, qui vaut false pendant tout cycle pair, et
true pendant tout cycle impair, comme le montre la figure ci-dessous.