| 1 | {{{ |
| 2 | #!html |
| 3 | <h1>TME 7 : Simulation logico-temporelle</h1> |
| 4 | }}} |
| 5 | [[PageOutline]] |
| 6 | |
| 7 | = Objectif = |
| 8 | |
| 9 | On souhaite réaliser dans ce TME un petit simulateur logico-temporel, permettant de |
| 10 | simuler un réseau Booléen temporisé, où les expressions Booléennes sont représentées par |
| 11 | des arbres EBM (voir TME3). |
| 12 | |
| 13 | Les simulateurs à événements discrets permettent de simuler des systèmes matériels |
| 14 | constitués d'un ensemble de composants matériels interconnectés par des signaux. Les |
| 15 | signaux véhiculent fondamentatement deux tensions VSS et VDD représentant respectivement |
| 16 | les valeurs Booléennes 0 et 1, mais le simulateur doit traiter plus de valeurs pour gérer |
| 17 | les cas spéciaux comme les signaux en haute impédance, les conflits électriques, ou les |
| 18 | valeurs indéfinies. A titre indicatif, le VHDL standard utilise 9 valeurs pour les |
| 19 | signaux. Pour simplifier, nous nous limiterons dans ce TME aux trois valeurs logiques 0, |
| 20 | 1, et U (indéfini). |
| 21 | |
| 22 | Dans le cas général, Chaque composant est modélisé par un processus, et tous les processus |
| 23 | s'exécutent en parallèle. Chaque processus utilise les valeurs de ses signaux d'entrées |
| 24 | pour calculer les valeurs de ses signaux de sortie. Un signal possède un seul émetteur, |
| 25 | mais peut avoir plusieurs destinataires. On définit, pour chaque processus, un |
| 26 | sous-ensemble des signaux d'entrée, appelé liste de sensibilité du processus : un |
| 27 | changement de valeur sur un signal appartenant à la liste de sensibilité peut entraîner un |
| 28 | changement de valeur sur un signal de sortie du processus. Un processus doit donc être |
| 29 | évalué à chaque fois que l'un des signaux de la liste de sensibilité change de valeur. Par |
| 30 | exemple, la liste de sensibilité d'un processus représentant un automate de Moore ne |
| 31 | comporte qu'un seul signal, qui est le signal d'horloge. |
| 32 | |
| 33 | On s'intéresse dans ce TME au cas particulier des réseaux Booléens: un processus |
| 34 | correspond à une expression Booléenne multi-niveaux. Dans ce cas particulier, un processus |
| 35 | possède donc un seul signal de sortie, et la liste de sensibilité contient tous les |
| 36 | signaux d'entrée. Cette liste de sensibilité est tout simplement le support de l'EBM, tel |
| 37 | que vu au TME3. Le réseau Booléen peut être représenté par un graphe biparti comportant |
| 38 | deux types de noeuds: des processus et des signaux. Les noeuds à la périphérie du réseau |
| 39 | sont toujours des signaux. |
| 40 | |
| 41 | [[Image(reseau_booleen.png, nolink)]] |
| 42 | |
| 43 | En d'autres termes, un processus a toujours au moins un signal entrant et un signal |
| 44 | sortant. Les signaux qui n'ont pas d'arrête entrante sont les entrées primaires du |
| 45 | réseau, les signaux qui n'ont pas d'arrête sortante sont les sorties primaires du réseau. |
| 46 | Les autres signaux sont appelés signaux internes. |
| 47 | |
| 48 | On appelle événement le changement de valeur d'un signal à un certain instant. Un |
| 49 | événement est donc défini par un triplet (signal, date, valeur). |
| 50 | |
| 51 | Simuler le fonctionnement d'un circuit consiste donc à calculer pour chaque signal la |
| 52 | succession des événements, appelée forme d'onde. L'ensemble des formes d'ondes de tous |
| 53 | les signaux constitue un chronogramme. |
| 54 | |
| 55 | La simulation suppose que l'on possède une fonction d'évaluation qui calcule la valeur du |
| 56 | signal de sortie du processus en fonction de la valeur des signaux d'entrée. Dans notre |
| 57 | cas, nous utiliserons la méthode {{{Ebm::eval()}}} qui gère les trois valeurs de signaux |
| 58 | (0,1,U). |
| 59 | |
| 60 | Créez un répertoire de travail TME5, et copiez dans ce répertoire les fichiers qui se |
| 61 | trouvent dans {{{/users/enseig/jpc/M1-CAO/TME/5.public}}}. |
| 62 | |
| 63 | |
| 64 | = A) Structures de données = |
| 65 | |
| 66 | On utilise deux structures de données pour représenter : |
| 67 | |
| 68 | * Le réseau Booléen ({{{BoolNet}}}), c'est à dire le graphe biparti des processus et des |
| 69 | signaux. |
| 70 | |
| 71 | * L'échéancier ({{{Scheduler}}}), c'est à dire l'ensemble ordonné des événements. |
| 72 | |
| 73 | |
| 74 | == A1) réseau Booléen == |
| 75 | |
| 76 | Un réseau booléen est la représentation d'un graphe bipartie. Il est donc constitué de |
| 77 | deux types de noeuds et d'arcs orientés reliants les noeuds entre eux. |
| 78 | |
| 79 | Les deux types de noeuds sont les ''signaux'' et les ''processus''. |
| 80 | |
| 81 | Un arc orienté relie un noeud source à un noeud cible. Notez que comme le graphe est |
| 82 | bipartie, les noeuds sources et destination sont toujours de types différents. On ne va |
| 83 | pas créer d'objet spécifique pour représenter un arc. Plus simplement, les noeuds sources |
| 84 | contiendront une liste de noeuds cible. |
| 85 | |
| 86 | * Un ''Signal'' considéré comme source, peut être utilisé dans un nombre quelquonque de |
| 87 | processus. il aura donc une liste de ''Processus'' cibles. |
| 88 | |
| 89 | * Un ''processus'' considéré comme source aura une unique cible: le ''signal'' dont il |
| 90 | calcule la valeur. Il n'est donc pas nécessaire de gérér une liste, un simple pointeur |
| 91 | suffira. Notez qu'il s'agit d'une conséquence de notre simplification du modèle qui |
| 92 | n'est pas applicable dans le cas général. |
| 93 | |
| 94 | '''La classe {{{BoolNet}}}''' |
| 95 | |
| 96 | En plus des accesseurs triviaux, elle fourni une fonction de recherche d'un signal par son |
| 97 | nom {{{getSignal(const std::string& )}}} ainsi que deux méthodes pour la construction du |
| 98 | réseau booléen. Le réseau booléen est initialisé vide. |
| 99 | |
| 100 | * Méthode {{{addSignal()}}}: ajoute un nouveau noeud de type ''signal''. On donne le nom |
| 101 | du signal ainsi que son type (parmis {{{In}}}, {{{Out}}} et {{{Internal}}}. La liste |
| 102 | des cibles du noeud est initialisée vide. |
| 103 | * Méthode {{{addProcess()}}}: ajoute un nouveau noeud de type ''processus''. On donne |
| 104 | respectivement comme arguments, le nom du ''signal'' cible, l'expression booléenne |
| 105 | qu'il représente (sous forme textuelle) et le delai de calcul de ce processus. |
| 106 | |
| 107 | '''Importante remarque''': la représentation des arcs (listes de cibles des noeuds |
| 108 | sources) sont construites lors de la création des processus. Il est donc impératif que |
| 109 | tous les ''signaux'' soient crées avant les ''processus''. |
| 110 | |
| 111 | {{{ |
| 112 | #!cpp |
| 113 | class BoolNet { |
| 114 | private: |
| 115 | std::string _name; |
| 116 | std::vector<Signal*> _signals; |
| 117 | std::vector<Process*> _processes; |
| 118 | public: |
| 119 | BoolNet ( const std::string& ); |
| 120 | inline std::string& getName (); |
| 121 | Signal* getSignal ( const std::string& ); |
| 122 | inline std::vector<Signal*>& getSignals (); |
| 123 | inline std::vector<Process*>& getProcesses (); |
| 124 | Signal* addSignal ( const std::string&, SignalType ); |
| 125 | Process* addProcess ( const std::string&, const std::string&, unsigned int delay ); |
| 126 | }; |
| 127 | }}} |
| 128 | |
| 129 | '''La classe {{{Signal}}}''' |
| 130 | |
| 131 | Attributs: |
| 132 | |
| 133 | * {{{_network}}} : le réseau booléen auquel elle appartient. |
| 134 | * {{{_variable}}} : l'{{{EbmVar}}} qu'elle encapsule. Le contructeur devra créer cette |
| 135 | {{{EbmVar}}} à la contruction. |
| 136 | * {{{_type}}} : le type du signal (parmis {{{In}}}, {{{Out}}} et {{{Internal}}}). |
| 137 | * {{{_processes}}} : la représentation des arcs. On choisi un {{{set<>}}} pour gérer |
| 138 | automatiquement l'unicité. |
| 139 | |
| 140 | Méthode non triviale: |
| 141 | |
| 142 | * {{{addProcess()}}} : ajoute une nouvelle cible à l'ensemble des cibles. Equivaut à |
| 143 | créer un arc dans le graphe. |
| 144 | |
| 145 | {{{ |
| 146 | #!cpp |
| 147 | class Signal { |
| 148 | private: |
| 149 | BoolNet* _network; |
| 150 | EbmVar* _variable; |
| 151 | SignalType _type; |
| 152 | std::set<Process*> _processes; |
| 153 | public: |
| 154 | Signal ( BoolNet*, const std::string&, SignalType ); |
| 155 | inline std::string getName (); |
| 156 | inline SignalType getType (); |
| 157 | inline ValueType getValue (); |
| 158 | inline std::set<Process*>& getProcesses (); |
| 159 | inline void addProcess ( Process* ); |
| 160 | inline void setValue ( ValueType ); |
| 161 | }; |
| 162 | }}} |
| 163 | |
| 164 | '''La classe {{{Process}}}''' |
| 165 | |
| 166 | Attributs: |
| 167 | |
| 168 | * {{{_network}}} : le réseau booléen auquel elle appartient. |
| 169 | * {{{_signal}}} : le signal qu'elle calcule. C'est la représentation de l'unique arc issu |
| 170 | d'un noeud de type ''processus''. |
| 171 | * {{{_expression}}} : l'{{{EbmExpr}}} de calcul. On la créera à l'aide de la méthode |
| 172 | {{{Ebm::parse()}}} qui vous est fournie. |
| 173 | * {{{_delay}}} : le temps nécessaire au calcul de la nouvelle valeur. Représente un temps |
| 174 | de propagation au travers des portes logiques. |
| 175 | |
| 176 | Méthodes non triviales: |
| 177 | |
| 178 | * {{{Process()}}} : le constructeur en plus de sa tâche d'initialisation des membres de |
| 179 | l'objet devra créer les ''arcs'' entre les ''signaux'' appartenant au support de |
| 180 | l'expression et le processus courant. |
| 181 | * {{{eval()}}} et {{{display()}}} sont des encapsulations des méthodes identiques de |
| 182 | l'objet {{{Ebm}}}. |
| 183 | |
| 184 | {{{ |
| 185 | #!cpp |
| 186 | class Process { |
| 187 | private: |
| 188 | BoolNet* _network; |
| 189 | Signal* _signal; |
| 190 | Ebm* _expression; |
| 191 | unsigned int _delay; |
| 192 | public: |
| 193 | Process ( BoolNet*, Signal*, const std::string& expression, unsigned int delay=0 ); |
| 194 | ValueType eval (); |
| 195 | inline Ebm* getExpression (); |
| 196 | inline Signal* getSignal (); |
| 197 | inline unsigned int getDelay (); |
| 198 | void display ( std::ostream& ); |
| 199 | }; |
| 200 | }}} |
| 201 | |
| 202 | |
| 203 | == A2) échéancier == |
| 204 | |
| 205 | Le rôle de l'échéancier est d'enregistrer et d'ordonner les évènements dans le temps. |
| 206 | Pour le réaliser nous avons besoin des éléments suivants: |
| 207 | |
| 208 | * Une date (classe {{{Time}}}) contenant le temps écoulé depuis le début de la simulation |
| 209 | en nano-secondes '''et''' un delta-cycle. Le delta-cycle permettant d'avoir plusieurs |
| 210 | simulations ''au même temps physique'' mais cependant séparés pour ne pas générer de |
| 211 | problèmes de causalité. |
| 212 | |
| 213 | * Un événement (classe {{{Event}}}), avec le temps ({{{Time}}}) auquel il se produit, Le |
| 214 | ''signal'' qu'il affecte et la nouvelle valeur que va prendre ce signal. |
| 215 | |
| 216 | * Une structure lui permettant de stocker les ensembles d'évenements par dates et de |
| 217 | trier ces ensembles par date (croissantes). Nous allons pour cela utiliser une |
| 218 | {{{map<>}}} de {{{vector<>}}}. C'est à dire, une {{{map<>}}} dont chaque élément sera |
| 219 | un {{{vector<>}}} d'évènements et la clé une date ({{{Time}}}. Notez qu'au sein d'un |
| 220 | ensemble d'évènements synchrones l'ordre est indifférent. |
| 221 | |
| 222 | '''Propriérés remarquables de la {{{map<>}}} de {{{vector<>}}}''' |
| 223 | |
| 224 | '''Syntaxe:''' |
| 225 | {{{ |
| 226 | #!cpp |
| 227 | map<Time, vector<Event*> > _events; |
| 228 | }}} |
| 229 | |
| 230 | La clé de tri est un objet de type {{{Time}}} et la valeur associée à cette clé un |
| 231 | {{{vector<Event*>}}}. Il faudra définir pour la classe {{{Time}}} une surcharge de |
| 232 | l'operateur ''strictement inférieur'' ({{{operator<()}}}) qui définira l'ordre |
| 233 | chronologique. |
| 234 | |
| 235 | '''Accès et itérateurs''' |
| 236 | {{{ |
| 237 | #!cpp |
| 238 | map<Time, vector<Event*> > _events; |
| 239 | map<Time, vector<Event*> >::iterator istate = _events.begin(); |
| 240 | |
| 241 | for ( ; istate != _events.end() ; ++istate ) { |
| 242 | const Time& date = (*istate).first; |
| 243 | vector<Event*>& events = (*istate).second; |
| 244 | |
| 245 | // Do something here. |
| 246 | } |
| 247 | }}} |
| 248 | |
| 249 | * '''Ordre du parcours''': une propriété fondamentale est que lors d'un parcours de la |
| 250 | {{{map<>}}} avec des itérateurs, les éléments sont parcourus ''dans l'ordre défini |
| 251 | par la relation d'ordre de la clé'', c'est à dire dans notre cas, l'ordre chronologique |
| 252 | de {{{Time}}}. |
| 253 | |
| 254 | * Cet ordre est maintenu automatiquent lors d'ajout ou de retrait d'élements dans la |
| 255 | {{{map<>}}}. |
| 256 | |
| 257 | * Les itérateurs pointant sur des éléments de la {{{map<>}}} restent valides même si l'on |
| 258 | ajoute ou retire des éléments (une seule exception: si l'on retire l'élement sur lequel |
| 259 | l'itérateur pointe...). |
| 260 | |
| 261 | * Les éléments d'une {{{map<>}}} sont des paires {{{(clé,valeur)}}}, pour y accéder à |
| 262 | partir de l'itérateur, il faut utiliser les attributs public {{{first}}} (clé) et |
| 263 | {{{second}}} (valeur). |
| 264 | |
| 265 | |
| 266 | '''La classe {{{Time}}}''' |
| 267 | |
| 268 | {{{ |
| 269 | #!cpp |
| 270 | // Time is used as key by the scheduler's map<>. |
| 271 | class Time { |
| 272 | private: |
| 273 | unsigned int _time; |
| 274 | unsigned int _deltaCycle; |
| 275 | public: |
| 276 | inline Time ( unsigned int time, unsigned int dc ); |
| 277 | inline unsigned int getTime () const; |
| 278 | inline unsigned int getDeltaCycle () const; |
| 279 | friend bool operator< ( const Time& lhs, const Time& rhs ); |
| 280 | }; |
| 281 | }}} |
| 282 | |
| 283 | |
| 284 | '''La classe {{{Event}}}''' |
| 285 | |
| 286 | L'attribut {{{_value}}} contient la ''prochaine'' valeur du signal. La méthode |
| 287 | {{{Event::updateSignalValue()}}} est chargé de faire la mise à jour effective de la valeur |
| 288 | du signal. Elle sera appelée dans l'étape de mise à jour du simulateur. |
| 289 | |
| 290 | {{{ |
| 291 | #!cpp |
| 292 | class Event { |
| 293 | private: |
| 294 | Signal* _signal; |
| 295 | ValueType _value; |
| 296 | Time _time; |
| 297 | public: |
| 298 | inline Event ( Signal*, ValueType, Time& ); |
| 299 | inline Signal* getSignal (); |
| 300 | inline ValueType getValue (); |
| 301 | inline Time& getTime (); |
| 302 | inline void updateSignalValue (); |
| 303 | }; |
| 304 | }}} |
| 305 | |
| 306 | |
| 307 | '''La classe {{{Scheduler}}}''' |
| 308 | |
| 309 | Méthodes non triviales: |
| 310 | |
| 311 | * {{{addEvent(Signal*,...)}}} : ajoute un nouvel évènement à l'échéancier. Pour donner la |
| 312 | date on ne passe pas d'objet {{{Time}}}, mais ses deux composants {{{time}}} et |
| 313 | {{{dc}}}. |
| 314 | |
| 315 | * {{{addEvent(const std::string&,...)}}} : une surcharge de la fonction précédente qui |
| 316 | prend comme premier argument un nom de ''signal'' au lieu du pointeur sur ''signal''. |
| 317 | Cette méthode sera utilisée préférentiellement pour initialiser l'échéancier. |
| 318 | |
| 319 | * {{{simulate()}}} : effectue la simulation. |
| 320 | |
| 321 | * {{{drive()}}} : écrit dans le flot donné en argument le résultat de la simulation dans |
| 322 | un format lisible par l'outil {{{xpat}}}. La définition de cette fonction vous est |
| 323 | fournie. |
| 324 | |
| 325 | * {{{_reset()}}} : remet toutes les variables (entrées, sorties, internes) à l'état |
| 326 | initial (U). |
| 327 | |
| 328 | * {{{_header()}}} et {{{_display()}}} : utilitaires vous permettant d'afficher l'état de |
| 329 | la simulation à un instant donné. |
| 330 | |
| 331 | {{{ |
| 332 | #!cpp |
| 333 | class Scheduler { |
| 334 | private: |
| 335 | BoolNet* _network; |
| 336 | std::map<Time, std::vector<Event*> > _events; |
| 337 | public: |
| 338 | Scheduler ( BoolNet* ); |
| 339 | Event* addEvent ( const std::string& variable, ValueType, unsigned int time, unsigned int dc=0 ); |
| 340 | Event* addEvent ( Signal*, ValueType, unsigned int time, unsigned int dc=0 ); |
| 341 | void simulate (); |
| 342 | void drive ( std::ostream& ); |
| 343 | private: |
| 344 | void _reset (); |
| 345 | void _header (); |
| 346 | void _display ( const Time& ); |
| 347 | }; |
| 348 | }}} |
| 349 | |
| 350 | |
| 351 | = C) Travail à réaliser = |
| 352 | |
| 353 | Dans un premier temps vous devrez utiliser le simulateur qui vous est fourni dans |
| 354 | l'ensemble des fichiers {{{.o}}}. |
| 355 | |
| 356 | Dans un second temps, il vous est demandé de progressivment remplacer les {{{.o}}} |
| 357 | fournis par les vôtres. |
| 358 | |
| 359 | |
| 360 | == C1) simulation du circuit ''etou'' == |
| 361 | |
| 362 | Le fichier ''etou.c'' contient un tout petit réseau Booléen ne contenant que deux noeuds, |
| 363 | et 4 signaux. Compilez ce fihier, et exécutez la simulation. |
| 364 | |
| 365 | Vous pouvez visualiser le réseau Booléen avec la commande: |
| 366 | {{{ |
| 367 | > eog etou.gif |
| 368 | }}} |
| 369 | Vous pouvez visualiser le chronogramme résulat de la simulation avec la commande: |
| 370 | {{{ |
| 371 | > xpat -l etou |
| 372 | }}} |
| 373 | |
| 374 | |
| 375 | == C2) Simulation d'un additionneur 2 bits == |
| 376 | |
| 377 | On se propose de simuler le schéma suivant, qui réalise un additionneur 2 bits. |
| 378 | A chaque porte est associé une expression Booléenne représentée par un EBM. |
| 379 | |
| 380 | [[Image(schema_portes.png, nolink)]] |
| 381 | |
| 382 | |
| 383 | == C2.1) Construction réseau Booléen de l'additionneur == |
| 384 | |
| 385 | En vous inspirant du fichier {{{etou.c}}}, écrivez le fichier {{{adder.c}}} qui construit |
| 386 | en mémoire le réseau Booléen correspondant au circuit additionneur 2 bits décrit |
| 387 | ci-dessus. On utilisera pour cela les fonctions {{{BoolNet::addProcess()}}} et |
| 388 | {{{BoolNet::addSignal()}}}. On prendra une valeur de 1 ns pour le temps de propagation de |
| 389 | la porte {{{NAND2}}}, et de 2 ns pour la porte {{{XOR2}}}. Pour vérifier la structure du |
| 390 | réseau Booléen, on utilisera la fonction {{{toDot()}}}. Cette fonction construit une |
| 391 | représentation graphique du réseau Booléen, et la sauvegarde dans un fichier au format |
| 392 | {{{.gif}}} ou {{{.ps}}}. |
| 393 | |
| 394 | Modifiez le Makefile permettant de compiler ce programme ''adder.c'', et exécutez-le. |
| 395 | |
| 396 | |
| 397 | == C2.2) Construction et initialisation de l’échéancier == |
| 398 | |
| 399 | Compléter le fichier {{{adder.c}}} {{{main()}}} pour créer l’échéancier et initialiser les |
| 400 | événements sur les signaux d’ entrée a0, b0, c0, a1 et b1 de façon à respecter le |
| 401 | chronogramme ci-dessous. On utilisera la fonction Scheduler::addEvent() et add_event(). |
| 402 | Attention : le passage de la valeur U (indéfinie) à une valeur 0 ou 1 constitue un |
| 403 | événement : Dans ce chronogramme, il y a donc un événement sur tous les signaux d’entrée |
| 404 | au temps {{{T = 0}}}. |
| 405 | |
| 406 | [[Image(chronogramme.png, nolink)]] |
| 407 | |
| 408 | Pour vérifier que l’échéancier est correctement initialisé, on pourra utiliser la fonction |
| 409 | ''Scheduler::drive()''. Cette fonction peut être utilisée avant même l'exécution de la |
| 410 | fonction de simulation, pour générer un fichier au format .pat qui décrit le chronogramme |
| 411 | des signaux d'entrée. Vous pouvez visualiser ce chronogramme avec l’outil {{{xpat}}}. |
| 412 | |
| 413 | |
| 414 | == C2.3) Simulation effective du circuit additionneur == |
| 415 | |
| 416 | Introduisez dans le fichier {{{adder.c}}} la méthode {{{Scheduler::simulate()}}} qui |
| 417 | effectue la simulation du réseau Booléen, jusqu'à ce qu'il n'y ait plus aucun événement à |
| 418 | traiter dans l'échéancier. Compilez ce programme, et analysez le chronogramme résultant |
| 419 | |
| 420 | |
| 421 | == C2.4) Ecriture de la boucle de simulation == |
| 422 | |
| 423 | Implémenter et remplacer progressivement toutes les classes qui vous on été fournies. |