| | 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. |