l'assembleur  [livre #4602]

par  pierre maurette



Structure
d'un
micro-ordinateur

1.1 Une machine électronique

Pour programmer en assembleur, ou même pour aborder un certain type de travail proche du matériel, quel que soit le langage utilisé, vous devrez envisager votre micro-ordinateur comme ce qu'il est avant tout : un équipement électronique qui pour ce qui concerne cet ouvrage héberge un système d'exploitation.

Notre but est de développer des applications devant fonctionner sur une gamme précise de matériel : les compatibles PC . Nous allons donc nous attacher à la description non pas détaillée d'une machine particulière, mais d'une machine moyenne, ou plus exactement d'un modèle consensuel de compatible, connu sous le nom de IA , Intel Architecture .

Cette architecture est l'objet du chapitre suivant qui sera dense et nécessitera certains acquis. Il faudra être familier des notions de bit, mot, mémoire, registre, instruction machine, système de numération binaire et hexadécimal, logique combinatoire de base. Ce sera l'objet de ce premier chapitre. Au lecteur de juger de l'opportunité de sa lecture.

Les premiers ordinateurs individuels, le ludique et déconcertant  ZX80 comme l'austère IBM PC , étaient également abondamment documentés, munis d'un langage de programmation en ROM, et en général de passerelles vers le langage machine. Ils étaient soutenus, modèle par modèle, par une presse dédiée et des groupes d'utilisateurs, dont certains existent encore aujourd'hui, du moins aux États-Unis.

La documentation de référence des premiers PC, d'origine IBM, est encore en 2004 utilisable pour des sujets comme la liaison série ou le clavier. Cette situation ne va pas s'éterniser.

Le problème actuel réside dans la complexité de la machine et le manque de documentation.

La compatibilité matérielle dans la galaxie PC est un sujet complexe, qui passe par diverses couches logicielles, et qui n'a rien à voir avec la monotypie offerte par un Apple 2 , un Oric 1 ou un Thomson MO5 .

Nous allons donc dans ce premier chapitre présenter des éléments d'un schéma à l'ancienne de microprocesseur dans son environnement. Cela nous permettra surtout d‘introduire les quelques notions indispensables pour la suite, et sur lesquelles nous ne reviendrons plus. Pour faciliter une lecture partielle, pas nécessairement dans l'ordre proposé, nous utiliserons pour chaque point un paragraphe clairement identifié, parfois un encadré, ce qui donnera à ce premier chapitre un aspect quelque peu disparate.

L'idée, discutable comme toute opinion non prouvée, est la suivante : il suffit d'avoir à l'esprit un modèle permettant d'admettre les phénomènes, à défaut de les comprendre. Le but sera atteint si, au cours de la lecture des pages qui suivent, le fonctionnement d'un micro-ordinateur perd son côté magique pour devenir au moins plausible .

1.2 Données analogiques et données binaires

Simplifions énormément : nous pouvons imaginer un courant électrique dans un fil conducteur comme la circulation d'un fluide, gaz par exemple, dans un tuyau. Ce fluide peut-être vu comme de petits grains d'électricité. Ce courant I se mesure en ampères A , c'est un débit, le nombre de grains par seconde traversant le fil. A la différence d'un gaz, cette électricité ne peut pas s'échapper, elle ne peut que circuler le long d'un chemin continu de fils conducteurs reliés. Si ce chemin est rompu par un interrupteur, le courant cesse. La tension U , mesurée en volts V , représente la force qui pousse les grains à circuler, comme la pression à l'intérieur d'un pneumatique. Une tension n'a de sens qu'entre deux points, reliés ou non. Une pile électrique peut être vue comme une pompe qui entretient une tension (de 1,5 V par exemple) entre ses deux bornes. Une résistance fait chuter la tension dans le conducteur d'autant plus que le courant est fort, comme un pincement dans un tuyau fait chuter la pression d'autant plus que le débit est important. Pour information, cette chute de tension U s'exprime en fonction de la résistance R et du courant I par la relation : U = R x I.

Prenons un fil électrique pouvant présenter par rapport à une référence de masse 0 V toute valeur de tension entre 0 V et 10 V. Pour fixer les idées, ce fil pourrait être la sortie d'un capteur de température. La quantité d'informations véhiculée par ce fil est théoriquement infinie. Intuitivement, la quantité d'informations est en rapport avec le nombre d'états différents que peut prendre le support. En fait, plutôt que différents, nous devrions écrire discernables ou distinguables.

Ce qui est vrai pour ce fil l'est pour un grand nombre de grandeurs dites analogiques , une intensité de courant électrique, la vitesse d'un véhicule, la quantité de liquide dans un verre ou une intensité lumineuse par exemple. Il existe des circuits permettant de faire du calcul directement à partir de ces valeurs. Cette façon de coder l'information apparaît donc excellente, mais présente quelques graves défauts :

  Elle est soumise à d'insidieuses dégradations dues à son transport et/ou simplement à la pollution de l'environnement électromagnétique. Certaines techniques et précautions peuvent améliorer cet état de fait.

  L'égalité de deux valeurs est aléatoire. Pour un couple de mesures, il existe toujours un niveau de précision qui les rend différentes et un autre qui les rend égales. Si nous voulons exprimer qu'une température mesurée est égale à une température de référence, ce sera toujours à une tolérance près. Même si la fourchette admise est minuscule, nous préférerions souvent disposer d'un nombre fini de valeurs possibles, pour qu'alors l'égalité de deux valeurs devienne indiscutablement vraie ou fausse.

  Ces valeurs analogiques sont parfaites pour alimenter un traceur de courbe. En revanche, stocker la valeur exacte de la température relevée chaque minute, en d'autres termes une fonction mémoire , pour calcul ultérieur, est compliqué. Attention, la mémorisation d'une valeur analogique par échantillonnage-blocage est un procédé classique, mais limité en durée et en nombre d'échantillons.

Prenons maintenant un fil électrique qui ne peut adopter que deux états : 0 volt ou 10 volts, allumé ou éteint, 0 ou 1, vrai ou faux, indifféremment pour l'instant. Nous sommes, vous l'avez compris, face à la brique élémentaire de notre futur système d'information. Cette unité d'information binaire élémentaire est généralement appelée bit , de l'anglais binary digit, soit tout simplement chiffre binaire. Il se trouve que de plus "a little bit" signifie en anglais "un petit peu".

Un petit point de vocabulaire : pour exprimer l'état d'un bit, il est courant d'entendre allumer , ou positionner un bit pour la mise à 1, éteindre ou parfois effacer un bit pour la mise à 0. Ceci à la fois en électronique et en informatique, quand le bit a une signification intrinsèque, typiquement un flag en informatique.

Ce nombre fini d'états est caractéristique du domaine du discret. Selon le contexte, les mots discret , numérique , logique , digital  sont plus ou moins synonymes. Discret est opposé à continu et a le sens le plus large, en mathématiques par exemple. Digital s'oppose à analogique et est plus spécifiquement technique, voire du domaine du marketing. Dans son état actuel, notre fil binaire est idéal pour rendre compte de l'état d'un feu de circulation. Il s'agit d'un signal logique .

Dans ce dernier cas, où le bit est signifiant par lui-même, où il correspond à une valeur logique, vraie ou fausse, on lui attribuera souvent, dans le microprocesseur, la dénomination flag , soit fanion.

À la question "Quelle est la température ?", il ne pourra répondre que par une valeur parmi 2. Il saura dire "Il fait chaud" ou "Il ne fait pas chaud". Nous allons donc devoir utiliser plusieurs fils à deux états pour exprimer plus précisément notre température : c'est à ce moment-là que nous parlons de numérique.

1.3 Les bus

Limité à 2 états : 0 et 1, notre fil bit0 est définitivement insuffisant pour une mesure. Que se passe-t-il si nous mobilisons un second fil, bit1 ? Combien d'états différents, donc de valeurs de température, pourrons-nous alors coder  ? Pour chacune des 2 valeurs 1 et 0 de bit1, bit0 pourra prendre 2 valeurs, soit 4 états au total. Dans l'ordre bit1 bit0, ces états sont : 00, 01, 10, 11. Ajoutons un troisième fil, bit2. Nous allons coder 2 fois plus d'états différents, soit 8, c'est-à-dire les 4 états précédents avec bit2 à 0, puis les mêmes avec bit2 à 1.

Codage sur trois fils
figure 1.01 Codage sur trois fils [the .swf]

Nous voyons que nous pouvons coder huit états différents et qu'à chacun de ces états, nous pouvons associer naturellement une valeur entière, de 0 à 7. Nous voyons également comment le nombre d'états possibles est doublé pour chaque bit ajouté. Ainsi, 4 fils feront 16 états différents, 5 en feront 32, etc. Cette suite 2, 4, 8, 16, 32, 64, 128, 256…, nous n'avons pas fini de la manipuler.

Nous appellerons bus  ces regroupements de fils, ou bits, par 4, 8, 16, etc. Ce n'est qu'un des sens possibles de ce terme, qui en réalité désigne le plus souvent un groupe d'un ou plusieurs fils, logiques ou analogiques, voire mélangés, qui "irriguent" un certain nombre d'entités. C'est par exemple le cas du câblage d'un réseau local.  

Remarque

Plus d'informations

Vous pouvez consulter maintenant l’ Annexe A consacrée à la numération pour plus de précisions sur la numération binaire. Y est présentée également la notation hexadécimale, qui n'est qu'une forme compacte du binaire, permettant une représentation alternative des nombres binaires à l'aide d'un chiffre par groupe de 4 bits. Nous y verrons également qu'un nombre est un nombre, et que seule sa représentation peut être qualifiée de binaire, octale, décimale ou hexadécimale.

Nous savons maintenant que N fils, ou bits, permettent de représenter 2 N (2 à la puissance N, soit 2 multiplié N fois par lui-même) états différents. Pour 8 bits, nous aurons 256 états différents. Nous voyons également comment, en comptant naturellement de 00000000 à 11111111, nous associons à ces états des entiers naturels, positifs. Pour 8 bits, nous comptons ainsi de 0 à 255. A l' Annexe A , nous voyons que nous pourrions interpréter ces 2 N états comme des entiers signés, de -2 N-1 à 2 N-1 -1, soit de -128 à +127 sur 8 bits.

À côté de cette valeur associée évidente, nous pourrions utiliser d'autres codages. Certains sont arbitraires, comme dans notre exemple les plages de température, dont le choix est de toute évidence en rapport intime avec le fondement de l'être humain. Ce codage pourrait en première approche être satisfaisant pour allumer des indicateurs ou commander des alarmes. Nous pouvons retenir de cet exemple qu'avec très peu de bits bien choisis  nous pouvons transmettre une information utile. Le nombre de bits nécessaire est d'autant plus faible que le choix du codage est judicieux. À quoi servirait, dans l'exemple précédent, de coder une température de 75°C ?

Une dernière remarque par rapport à cet exemple : nous avons converti une valeur analogique de température en une valeur numérique. À l'inverse, à chaque valeur numérique correspond une plage de valeurs analogiques et, cela, indépendamment de l'imprécision de la mesure. Un appareil de mesure à affichage numérique est trompeur, parce que s'il affiche 1,45, nous risquons de vouloir oublier la signification, qui est par exemple "entre 1,445 et 1.455", même s'il est parfait.

Anticipons: certains codages sont reconnus par le microprocesseur, en ce sens que des instructions n'ont de sens que dans cette interprétation. C'est le cas des entiers naturels et des entiers signés. D'autres n'existent que dans le langage utilisé pour programmer ou simplement dans le cerveau du programmeur.

Pour décrire l'état d'un ensemble de bits, donc comme nous le verrons celui d'une case mémoire, nous utilisons par défaut les entiers naturels, exprimés en binaire, mais le plus souvent en hexadécimal ou décimal. Si les bits d'une mémoire ont les états 11000111, nous écrirons C7 en hexadécimal, ou 200 en décimal pour décrire la réalité physique. Mais selon qu'elle sera interprétée comme un entier naturel ou comme un entier signé, nous écrirons que la mémoire continent la valeur 200 ou la valeur -57.

Remarque

Évolution des largeurs de bus dans les microprocesseurs réels

Une largeur de bus de 4 bits, comme dans les tout premiers microprocesseurs intégrés, permettait théoriquement de tout fabriquer, mais essentiellement des calculatrices et des automates. À partir des microprocesseurs 8 bits, qui permettaient de motoriser de vrais micro-ordinateurs, l'évolution suit à chaque étape le même schéma : un doublement de la largeur du bus de génération en génération.

Ce doublement n'est certainement pas lié uniquement à la loi de Moore, que nous évoquons en fin de chapitre. Nous pouvons y voir davantage la trace de la recherche d'une compatibilité ascendante entre les diverses générations. Pour en rester à l'architecture Intel, donc AMD, ce doublement est nécessaire pour qu'une machine de la génération N soit virtuellement présente au sein de la machine de génération N+1. Cela est d'autant plus vrai qu'il existe toujours dans une génération les germes de la suivante : dans notre processeur 32 bits actuel, un certain nombre d'instructions reconnaissent une largeur d'entier de 64 bits.

Comme pour la loi de Moore, nous pouvons penser que l'évolution de la largeur du bus de l'unité centrale est proche de sa fin : pour décrire une case mémoire dans une UC 512 bits, il ne faudrait pas moins de 128 caractères hexadécimaux. Nous pourrions plutôt songer à des UC raisonnables couplées à des unités de calcul à grand parallélisme et à forte spécificité : calcul, graphisme, traitement du signal…

1.4 Les horloges

Avec nos fils à deux états, nous continuons à utiliser une tension électrique dans un (semi)conducteur. Nous restons donc d'une certaine façon dans le domaine de l'analogique. Nous avons simplement décidé de forcer l'interprétation binaire de cette valeur. Il existe dans la nature des objets fondamentalement discrets (non, pas les espions), dans les domaines de la biologie moléculaire ou de la physique des particules par exemple. Ces objets ne sont pas systématiquement binaires (2 états). Ils sont même souvent ternaires (3 états). Mais cela est de la science-fiction ou au moins de la prospective.

Conservons pour l'exemple nos 0 V-10 V. Décidons que les tensions de 0 V à 1 V seront lues comme un niveau 0 logique, et celles de 9 V à 11 V comme un niveau 1 logique. Les autres valeurs provoquent des résultats imprévisibles. Malheureusement, dans le monde réel, relevant du domaine de l'analogique, notre fil électrique passera par toutes les valeurs intermédiaires pour basculer du 0 au 1, et inversement, durant le temps de basculement .

Qu'à cela ne tienne, créons un signal, c'est-à-dire un fil, logique également, qui change d'état de façon périodique. Appelons ce signal une horloge . S'il accomplit son cycle 1 million de fois par seconde, nous parlerons d'une horloge de fréquence 1 MHz. À chaque cycle de cette horloge, nous pouvons distinguer au moins 2 moments caractéristiques, par exemple l'atteinte de l'état haut et l'atteinte de l'état bas. Décidons dès lors, à un de ces instants, de lire tous les fils simultanément. Pour l'instant, nous sommes simplement devant des fils indépendants, sans trop savoir à quoi correspondent les notions de lecture et d'écriture, ni même ce qui provoque un changement d'état. Retenons simplement que :

  Un temps de basculement court est un facteur fondamental de qualité.

  Les fils binaires seront dans un état stable et défini, donc exploitable, à un moment précis d'un signal d'horloge.

  Il y a un rapport intime entre performances élevées, temps de basculement faible et fréquence du signal d'horloge élevée.

Trois signaux de qualités différentes
figure 1.02 Trois signaux de qualités différentes [the .swf]

Le signal du bas est parfait, celui du milieu correct et celui du haut de mauvaise qualité. Il y a respectivement 7, 5 et 3 demi-périodes dans la même tranche de temps. La quantité d'informations véhiculée est en rapport. Ce dessin n'a qu’un intérêt visuel. Il ne prouve pas grand-chose en réalité.

Attention, si vous avez entendu parler de logique combinatoire et de logique séquentielle, vous savez que cette dernière est caractérisée par une horloge ; ce n'est pas exactement celle que nous venons d'évoquer.

Anticipons sur le paragraphe suivant : si un au moins des signaux d'entrée d'une porte en logique combinatoire est à l'état imprévisible, la sortie est également inexploitable. Notre horloge s'applique sur la sortie de la porte en même temps que sur ses entrées, dans la mesure où le retard pris par le signal dans la porte est négligeable. Et sinon ? Sinon, c'est tout simple, il suffit de lire la sortie au coup d'horloge suivant, en n'ayant pas modifié les entrées entre-temps. Le traitement aura coûté 1 cycle d'horloge. Nous pourrions ici envisager une sous-horloge, ou une horloge à plusieurs phases, mais cela nous amènerait trop loin sur le chemin de l'électronique. C'est néanmoins bien de cette façon que fonctionnent la plupart des circuits de nos ordinateurs.

Ce qu'il faut retenir, c'est que nous disposerons d'un ou plusieurs signaux d'horloge, et que le traitement sera une succession de temps de modifications, instables, intercalés entre des phases de validation, stables.

1.5 Traitements sur les bits et les bus

L’ Annexe B est consacrée à la logique combinatoire. Les notions élémentaires de tables de vérité et de fonctions logiques y sont abordées. Elles sont indispensables pour lire les lignes qui vont suivre. De plus, elles sont également incontournables en programmation.

Sur un signal binaire, nous pouvons facilement, au sens de la technologie, effectuer des opérations logiques élémentaires. Connaître ces opérations est important pour nous pour au moins deux raisons : comprendre comment le microprocesseur peut effectuer des traitements à la demande, comme une addition, et approcher son fonctionnement général, accès au code et aux données en particulier.

Comme la brique élémentaire d'information est le bit, la brique élémentaire du traitement est la porte logique. Nous pouvons la représenter sous la forme d'une boîte, dans laquelle entrent 1 ou 2 bits, et en sort 1 bit. L' Annexe B donne l'ensemble des fonctions possibles avec 1 et 2 bits en entrée.

Penchons-nous sur le OU EXCLUSIF.

La fonction OU EXCLUSIF
figure 1.03 La fonction OU EXCLUSIF [the .swf]

Nous avons reproduit ici la table de vérité, que vous trouverez dans l' Annexe B . Rappelons également que le OU EXCLUSIF, ou XOR, se traduit par : A ou B, mais pas les deux à la fois.

La première représentation est classique ; elle montre deux entrées et une sortie, la sortie se déduisant de la table de vérité.

Le second dessin représente strictement le même circuit. S se déduit de E et de commande, à l'aide de la table de vérité. Observons donc justement cette table de vérité : si commande est à 0, alors la sortie S est égale à l'entrée E. Si, en revanche, commande est à 1, la sortie sera l'inverse de l'entrée. Nous avons donc une porte une entrée/une sortie, dont nous pouvons choisir la fonction par un bit de commande. Quel est l'intérêt de cette manipulation ? Simplement de montrer qu'il sera possible de modifier le comportement d'un bloc logique par un ou plusieurs bits de commande.

Nous avons vu qu'une sortie peut être positionnée à 1 ou à 0. Mais la technologie des bus nous impose en plus de pouvoir complètement couper une sortie. On dit qu'elle est dans le troisième état (ce type de sortie s'appelle three-states  en anglais), ou mise à haute impédance. Le cas typique est le bus de données. Un très grand nombre d'entités sont susceptibles de fournir le même bus de données, mais une seule entité au même instant. Qu'elles fassent ou non partie de circuits physiquement indépendants, elles sont toutes branchées sur ce bus. Celle qui est sollicitée à un instant précis est déterminée par le bus d'adresses. Mais il ne faut pas que les sources inactives viennent perturber ce bus pendant ce temps.

Sortie 3 états
figure 1.04 Sortie 3 états [the .swf]

Cette représentation est hautement symbolique. Il s'agit bien entendu d'une action sur les transistors de sortie, la coupure physique étant un idéal.

Remarque

Il est possible de provoquer une panne par logiciel

Cela a du moins été possible. Deux sorties logiques ne sont normalement pas conçues pour être connectées l'une à l'autre, sous peine de destruction. Or, certains micro-ordinateurs balayaient le clavier en configurant des pattes en entrée et en sortie alternativement. Il n'était pas très compliqué, avec quelques lignes d'assembleur, de les configurer simultanément en sortie. La parade, au niveau du schéma électronique, est simple et a certainement été très vite apportée.

Il faut surtout retenir des sorties trois états que la même logique qui informe un circuit qu'il est sélectionné devra imposer aux autres de s'isoler, pour ne pas perturber le bus. Nous présentons cette particularité dès maintenant, car nous allons l'utiliser dans la présentation de l'unité de calcul, que nous abordons maintenant. Rappelons à ce stade que nous ne cherchons qu'une chose : à convaincre que le fonctionnement d'un micro-ordinateur ne relève pas de la magie.

Consultez au besoin, dans l' Annexe A , la partie consacrée à l'addition binaire. Observez ce que nous y appelons une table d'addition.

Addition binaire
figure 1.05 Addition binaire [the .swf]

Cette table d'addition ressemble énormément à la table de vérité d'un XOR. La porte XOR est d'ailleurs parfois appelée demi-additionneur . Quant à la retenue, elle n'est à 1 que quand les deux entrées sont en même temps à 1, ce qui décrit le comportement d'une porte AND. Dessinons le circuit correspondant.

Le demi-additionneur
figure 1.06 Le demi-additionneur [the .swf]

Vous trouverez souvent les schémas du demi-additionneur et de l'additionneur réalisés à base exclusive de circuits NAND. La justification se trouve en Annexe B  ; mais, dans notre cas, cela ne servirait qu'à nous embrouiller, si ce n'est déjà fait.

Si maintenant nous désirons un additionneur complet, il est tentant d'appliquer le même traitement au résultat Somme et à une retenue antérieure. Mais quid des retenues ?

Si A et B génèrent une retenue, alors Somme vaut 0 (cf. table d'addition). Nous en déduisons facilement que les deux demi-additionneurs ne pourront pas générer de retenue simultanément. Cette constatation toute simple et heureusement vraie quel que soit le système de numération sera utile à la compréhension du fonctionnement certaines instructions de notre microprocesseur. Elle nous permet de dessiner un modèle d' additionneur complet.

Additionneur complet
figure 1.07 Additionneur complet [the .swf]

Il nous reste maintenant à tester cette cellule élémentaire, en l'utilisant plusieurs fois égale à elle-même, dans l'addition de mots de N bits.

Additionneur 8 bits
figure 1.08 Additionneur 8 bits [the .swf]

Opérande A et Opérande B représentent des registres, c'est-à-dire dans ce cas 8 bits disponibles en sortie. Résultat est également un registre, prêt à accueillir et à conserver un certain temps 8 bits également. Les registres sont des mémoires, objet du paragraphe suivant.

Dans cette approche de l'additionneur 8 bits, la retenue entrant dans la première cellule à droite pourrait provenir d'une case mémoire 1 bit affectée à cet effet, dans laquelle serait stockée la retenue sortant de la dernière cellule à gauche lors d'une addition antérieure.

Nous sommes encore loin d'une instruction du microprocesseur, puisqu'il manque certainement des registres en tampon et beaucoup d'autres éléments. Nous verrons par exemple que, dans l'opération réelle d'addition, le résultat n'est pas stocké dans un registre dédié, mais vient remplacer l'opérande A.

Vous devez retenir que le circuit représenté ici ne nécessite que quelques briques élémentaires, sur la puce, pour être implémenté.

Le cerveau de notre microprocesseur, et non pas son cœur, qui serait plutôt le système d'horloge, ressemble un peu à ce circuit d'addition. Nous le nommons Unité Arithmétique et Logique , soit  UAL , et plus couramment  ALU . Imaginons que nous disposions de plusieurs modèles de circuits, chacun effectuant une opération différente entre un ou deux registres, et déposant le résultat dans un troisième. Le type d'opération à accomplir nous est communiqué sous forme de bits, chacun désignant une opération, un et un seul de ces bits étant allumé à chaque instant. Voyons à quoi pourrait ressembler le fonctionnement statique de cette ALU.

Une ALU très simplifiée
figure 1.09 Une ALU très simplifiée [the .swf]

Nous avons représenté un embryon de registre d'état, sous la forme d'un bit C, comme Carry , c'est à dire retenue, destiné à conserver la retenue de l'addition. Remarquons que la fonction NOT n'a besoin que d'un seul opérande.

Pourquoi parlions-nous de fonctionnement statique ? Parce que ce que nous venons de voir n'est qu'un instant précis du processus de calcul. Il a été précédé d'une phase d'approvisionnement des différentes données, y compris celle du type d'opération à effectuer. Elle sera suivie d'au moins une phase de rangement du résultat. Tout cela s’effectue au rythme de l'horloge, une tâche à la fois, chacune entraînant inéluctablement la suivante. Dans cet exemple, le moment précis de l'opération pourrait être un signal d'horloge validant l'écriture du résultat, une fois les autres organes stabilisés à l'issue des phases précédentes.

La complexité de cette ALU se trouverait considérablement augmentée si nous lui ajoutions ces quelques éléments de séquencement. Elle resterait néanmoins dérisoire par rapport à celle d'un simple Pentium.

Il est amusant de constater que, dans la conception de puces de cette complexité à un coût raisonnable, l'informatique est le but, mais également le moyen. C'est certainement grâce à de puissants programmes que des équipes peuvent les dessiner, personne ne maîtrisant totalement la globalité du produit.

Nous n'avons pas encore abordé la notion de programme, mais nous en savons un peu plus sur le fonctionnement intime du microprocesseur. Trop peu pour comprendre comment il fonctionne, mais suffisamment pour admettre qu'il puisse fonctionner.

Nous pouvons ici évoquer quelques fonctions de l'électronique des bus. Leur connaissance n'est pas indispensable au programmeur, mais elles sont au cœur de l'accès à la mémoire. Or, l'accès à la mémoire n'est pas le sujet le plus simple dans l'étude du 8086/8088. De plus, ces notions font appel à une façon de voir, à une terminologie, auxquelles vous serez confronté en lisant de la documentation.

Nous avons défini un bus comme un regroupement de fils, de cases mémoire, de bits donc. En vérité, comme nous l'avons déjà signalé, le mot bus désigne une façon de distribuer le signal : il n'est pas exclu de parler d'un bus à un seul fil, dans la mesure où il distribue l'information d'une certaine façon. Un bus est un support d'information qui dessert un certain nombre de points d'utilisation, à charge pour chacun de ces points d'en extraire (ou d'y déposer) l'information qui le concerne.

D'un microprocesseur partent deux bus : celui d'adresses et celui de données. Il est courant d'ajouter le bus de contrôle, comme le regroupement des signaux de gestion des échanges sur les deux autres. Ils irriguent un certain nombre de circuits, d'unités logiques. Chacune de ces unités tire du bus d'adresses l'information déterminant si le processeur s'adresse ou non à elle, et à quel emplacement de sa mémoire il souhaite accéder. Selon le sens de l'échange souhaité, lecture ou écriture, l'unité va prendre ou déposer l'information sur le bus de données.

Remarque

Architecture Von Neuman, architecture Harvard

Nous parlons d'un bus de données et d'un bus d'adresses. Il est clair que le bus d'adresses permet de se situer au bon endroit dans la mémoire, pour ensuite la lire et déposer le résultat sur le bus de données ou y écrire la valeur présente sur ce bus.

Voilà pour le bus de données ; mais qu'en est-il des instructions ? Au niveau de l'accès en mémoire physique, aucune différence n'est faite entre données et instructions, au sens programmation du terme. Le bus de données, ou Data Bus, va donc véhiculer, en lecture ou en écriture, le contenu de la mémoire, qu'il s'agisse de données ou d'instructions. Cela est caractéristique de l'architecture Von Neumann .

Cette architecture est utilisée pour tous les microprocesseurs à usage de micro-ordinateurs. Néanmoins, comme elle n'est pas exempte de défauts, les microprocesseurs récents vont mettre en place des stratégies de protection de certaines zones de la mémoire. Nous y reviendrons.

Ces protections interdisent notamment que le code puisse se modifier lui-même, possibilité parfois présentée à tort comme un avantage de cette architecture. Il est effectivement possible de prévoir, à la fin d'un code d'initialisation, une modification au niveau d'une instruction de saut aboutissant à ce que le programme ne repasse plus par ce code.

L'alternative est l'architecture Harvard , qui sépare physiquement le programme et les données. Elle se rencontre en particulier dans la majorité des DSP, des processeurs de traitement du signal, ainsi que dans les microcontrôleurs de la famille PIC, qui seront présentés dans cet ouvrage. Il sera ainsi possible d'avoir le programme dans une cartouche de mémoire ROM ou EPROM, et les données partagées entre la RAM ordinaire et de la RAM sauvegardée en cas de coupure de l'alimentation. Cette séparation est tellement importante dans certains contextes que les automates industriels, bâtis autour de microprocesseurs classiques, de type Von Neumann, construisent autour une véritable architecture Harvard virtuelle.

Dans le cas général, les éléments du bus d'adresses partent du microprocesseur et sont distribués en permanence à toutes les unités clientes. Tout au plus les signaux pourront-ils être amplifiés, si le nombre de circuits récepteurs est important en regard des caractéristiques électriques du microprocesseur.

Imaginons un processeur dont le bus d'adresses mesure 10 bits, donc un espace adressable de 1 Ko, 1 024 octets. Il est en tout et pour tout accompagné de 4 circuits mémoire de 256 octets chacun. Le bus d'adresses de chacun de ces circuits mesure donc 8 bits. Les 2 bits de poids fort qui ne desservent pas les boîtiers seront utilisés pour choisir un boîtier parmi 4.

Décodage d'adresse simple
figure 1.10 Décodage d'adresse simple [the .swf]

L'entrée CS, Chip Select , est une entrée de validation du boîtier. D'autres signaux peuvent être nécessaires pour que cette validation soit effective. Le CS est remplacé sur certains circuits par 2 ou 3 entrées, CS0, CS1, CS2. Certaines de ces entrées sont actives à l'état haut, d'autres à l'état bas. Cela peut permettre d'éviter un décodage, en câblant judicieusement les bonnes pattes d'adresse sur les bons CS.

Un décodeur est un type de circuit qui allume une et une seule de ses sorties, celle dont le numéro correspond au chiffre représenté, en binaire, par les entrées (les entrées sont également numérotées). Il existe donc des décodeurs 4 vers 16 ; il pourrait en exister en 8 vers 256, mais ils devraient avoir beaucoup de pattes.

Si A8 et A9 sont à 0, ce qui est le cas dans les 256 premiers octets de l'espace adressable, la sortie 0 sera allumée et le chip mémoire de gauche sera sélectionné. Ce boîtier occupe donc les adresses de 000h à 0FFh. Les autres seront situés à la suite : 100h à 1FFh, 200h à 2FFh, 300h à 3FFh. Ainsi, tout l'espace mémoire sera couvert.

Il est aujourd'hui facile de décoder un bus d'adresses à l'octet près, à l'aide de réseaux logiques programmables spécifiques. À ces circuits, un décodeur 8 vers 256 ne fait pas peur.

Nous avons déjà évoqué la mise en haute impédance (troisième état) des interfaces de bus de données non utilisées. Pour lire une donnée (qui, rappelons-le, peut être une instruction), le processeur va générer et présenter l'adresse, donc sélectionner un boîtier et positionner son bus de données en entrée. À ce moment-là, tout est normalement à l'état haute impédance sur ce bus. Enfin, certains signaux du bus de contrôle vont provoquer la mise en sortie des pattes du boîtier sélectionné et la récupération de la donnée par le processeur. L'écriture n'est pas plus compliquée. En réalité, la chronologie d'accès mémoire est plus complexe que ce qui vient d'être décrit, mais cela nous importe peu.

Nous trouvons généralement, sur le bus de données, des amplificateurs de bus bidirectionnels (bus transceivers). Ils sont connectés au bus de contrôle et peuvent jouer le même rôle que la mise de ce bus en haute impédance, ou la remplacer si elle n'existe pas.

Le 8086/8088 n'est pas le plus ordinaire des processeurs, et les schémas précédents devront être modifiés pour prendre en compte le multiplexage des bus.

Mais, auparavant, définissons la fonction verrou ou verrouillage, ou encore latch , mot anglais souvent adopté en français. Cette fonctionnalité consiste à saisir une valeur, sur un fil ou sur un bus, au bon moment. Il faut pour cela que la valeur soit accompagnée d'un signal supplémentaire qui la valide. C'est généralement un front montant ou descendant (passage de l'état bas à l'état haut ou inversement) qui signale l'instant du latch. Le rapport avec l'horloge est évident.

Un latch peut être un circuit n'ayant que cette fonction, en fait une mémoire d'un seul mot. Mais c'est également souvent une fonctionnalité rajoutée à une entrée de circuit ayant d'autres fonctions.

Nous prendrons comme exemple le bus du 8086/8088. C'est un bus de 20 bits qui n'est bus d'adresses que pendant une petite partie du cycle d'accès mémoire. Le reste du temps, il véhicule les données. Nous dirons qu’adresses et données sont multiplexées sur le même bus.

Le problème, vu du bus d'adresses, est double : savoir à quel instant l'adresse est valide et conserver la valeur de cette adresse pendant le reste du cycle. Heureusement, le microprocesseur fournit tous les signaux utiles.

Verrouillage du bus d'adresses
figure 1.11 Verrouillage du bus d'adresses [the .swf]

Nous avons simplement repris le schéma précédent, en considérant que le bus de données et le bus d'adresses se partagent le même bus physique. Le circuit de latch isole une zone bus d'adresses, le verrouillage étant synchronisé par un signal ALE, comme Address Latch Enable .

Pour faire fonctionner un 8086/8088, seul le latch du bus d'adresses est absolument nécessaire. Le bus multiplexé accompagné des signaux de contrôle adéquats peut attaquer directement les circuits mémoire, dans une configuration légère. Ce n'est pas ce qui a été fait dans l'IBM PC.

1.6 La mémoire

Il est facile de mémoriser la valeur d'un bit dans un composant appelé bascule, une bascule D issue d'une bascule R-S en l'occurrence. Il est possible de représenter très simplement une cellule élémentaire de ce type.

Cellule mémoire de base
figure 1.12 Cellule mémoire de base [the .swf]

La valeur de l'entrée est verrouillée au moment précis d'un événement comme un changement d'état, ou front sur le signal Strobe, et ensuite présentée de façon permanente en sortie.

Ce type de mémoire, à base de bascules, est intégré dans le reste de la logique, donc sur les mêmes bases de temps d'accès. Regroupées par paquets de taille encore raisonnable, elles constituent en particulier les registres de l'unité centrale. Dans celle-ci, un grand nombre de ces cellules participent discrètement à la cuisine interne. Le signal Strobe est issu du circuit général d'horloge.

Pour parvenir à de grandes capacités de stockage, d'autres solutions que les bascules sont nécessaires. Cela se fait au détriment de la vitesse d'accès. À un stade donné de l'évolution technologique, capacité et vitesse sont des qualités évoluant en sens contraire. Certaines techniques (cache) permettent de mettre à profit cette dualité pour améliorer les performances, en mélangeant un peu de mémoire rapide et beaucoup de mémoire lente. Le but est d’allier la vitesse de l'une et la capacité à faible coût de l'autre. Le problème est de charger le cache, indépendamment des accès de l'unité de calcul, mais en faisant en sorte que cette dernière trouve le plus souvent son bonheur dans le cache.

Le type de mémoire le plus courant ( RAM ) permet la lecture et l'écriture à peu près dans les mêmes conditions. D'autres mémoires ( ROM ) sont pourvues de données à la fabrication et ensuite ne peuvent qu'être lues. Certains types intermédiaires ( PROM , EPROM , EEPROM …) permettent l'effacement et la réécriture, ou plus exactement la reprogrammation. Reprogrammables ou pas, ces mémoires sont équivalentes pour le programmeur à de la ROM, mémoire à lecture seule.

La mémoire nous apparaît sous la forme de circuits intégrés ou de modules. Deux chiffres caractérisent chacune de ces unités : le nombre d'éléments mémorisables, déterminant la largeur du bus d'adresses et le nombre de bits de chacun de ces éléments, donnant lui la largeur du bus de données. Par exemple, un boîtier de 64 octets et un autre de 512 (= 64 x 8) bits auront la même capacité, et à priori à peu près le même prix, mais leur structure et leur utilisation seront différentes.

Le premier aura un bus d'adresses de 6 bits de large et un bus de données de 8, alors que le second aura des bus de 9 et 1 bit respectivement. Les valeurs exemples ne sont pas réalistes, nous pouvons ajouter à la lecture un Kilo, pour obtenir des valeurs cohérentes avec l'IBM PC historique, ou un Méga pour des valeurs plus actuelles.

Deux boîtiers de même capacité
figure 1.13 Deux boîtiers de même capacité [the .swf]

Ces deux circuits comportent les mêmes 512 cellules mémoire élémentaires, mais les gèrent différemment. Quelques signaux de contrôle participent à cette gestion. Ils indiquent si une opération est une lecture ou une écriture, puisque le bus de données est bidirectionnel, le moment où adresses et/ou données sont valides, etc. Une ou plusieurs pattes de type CS ( Chip Select ) ou OE ( Output Enable ) permettent d'isoler complètement le circuit des bus par une mise de ceux-ci dans l'état haute impédance tant que le circuit n'est pas exploité.

Pour une mise en œuvre sur un ordinateur 8 bits, il sera possible d'utiliser un seul boîtier de 64 octets, pour une mémoire totale de 64 octets ; mais il faudra au minimum en installer 8 de 512 bits, pour une mémoire, il est vrai, de 512 octets. Nous pouvons obtenir le même plan mémoire à l'aide de boîtiers de 64 octets :

Deux voies différentes pour le même résultat
figure 1.14 Deux voies différentes pour le même résultat [the .swf]

Le premier schéma appelle peu de commentaires : le bus d'adresses attaque les 8 circuits simultanément, chacun pouvant lire ou écrire un bit parmi 512.

Dans le second, ce sont les 6 bits de poids faible du bus d'adresses qui sont distribués à tous les circuits, pour sélection d'un octet parmi 64 dans chacun d'eux. Les 3 bits d'adresse restants permettent de choisir, à l'aide de la patte CS, dans lequel des huit circuits sera choisi l'octet. En effet, nous avons bien 8 valeurs différentes sur 3 bits.

1.7 Le programme

Nous avons vu qu'il était possible d'effectuer sur deux données numériques binaires des opérations dont la nature est elle-même représentée par une donnée numérique binaire. Parmi ces opérations, il en est de plus simples que les calculs : ce sont les simples transferts mémoire. Il s’agit de transferts entre mémoire et registre et entre registre et registre. Généralement, les transferts entre mémoire et mémoire n'existent pas physiquement.

Ces opérations peuvent être conditionnées à l'état d'un bit : si le bit est allumé, l'opération se fait d'une certaine façon, s'il est éteint, elle se fait d'une autre.

Nous oublions Turing et ses théories : si nous en sommes à avoir un intérêt pour l'assembleur, c'est que nous admettons qu'un enchaînement bien pensé d'actions élémentaires conduit à un algorithme, qui lui-même aboutit souvent à la résolution de problèmes. Attention, nous venons d'éluder en une phrase une des bases de l'algorithmique. Le sujet est en réalité vaste et passionnant.

Nous allons donc stocker à la queue leu leu les opérations que nous désirons réaliser sur nos données dans la mémoire, opérations au sens large : code de l'opération, plus les opérandes. Nous dirons instruction plutôt qu'opération.

Nous décidons de gérer un registre particulier, le compteur programme , et l'appellerons IP, comme Instruction Pointer.

C'est à un endroit désigné par ce registre que nous irons à la fin de chaque instruction chercher la suivante. Il sera donc augmenté de la taille de l'instruction au chargement de celle-ci.

Il semble bien qu'il soit possible, si le registre IP est modifié par une instruction particulière de temps en temps, de faire dépendre le déroulement du programme de résultats obtenus en cours d'exécution, de faire des choix.

Et, encore une fois, tout cela se déroule inéluctablement, au rythme des horloges, sans aucune place laissée au hasard.

Maintenant, nous pouvons aller sur Internet et nous renseigner sur Babbage, Turing et consorts.

1.8 Les entrées/sorties

Les entrées/sorties , E/S ou I/O , se font par l'intermédiaire de circuits spécialisés, les coupleurs d'entrée/sortie . Électroniquement, ces circuits sont des mémoires de très faible capacité, quelques registres le plus souvent. Le programmeur ne saura rien effectuer d'autre que de lire et d'écrire ces registres, muni bien entendu de la documentation du circuit.

Les registres internes des coupleurs d'E/S sont souvent de 8 bits de large, même si le processeur est d'une technologie 16, 32 ou 64 bits. Le plus souvent, les circuits coupleurs d'E/S peuvent générer un signal d'interruption. La notion d'interruption sera présentée en fin de chapitre.

Prenons pour exemple le circuit de type 8250, qui est le coupleur série de l'IBM PC. C'est au travers de ce circuit, ou d'un modèle actuel compatible au niveau du logiciel, que l'ordinateur se connecte à Internet via un modem.

ch01_15.eps  Les registres d'un coupleur série
figure 1.15 ch01_15.eps  Les registres d'un coupleur série [the .swf]

La valeur à envoyer sera à écrire très logiquement dans le tampon d'émission, alors qu'il faudra lire la valeur reçue dans le tampon de ... réception. D'autres registres permettent de configurer et de surveiller ligne et modem.

Il est possible d'indiquer au coupleur les événements susceptibles de provoquer une interruption : erreur de transmission par exemple, mais surtout tampon de réception plein ou tampon d'émission vide. En anticipant un peu, remarquons que l'intérêt en est évident : le programme pourra travailler à quelque chose d'utile pendant les opérations d'émission et de réception, qui sont très longues à l'échelle de l'ordinateur. Un registre renseignera le processeur sur le type d'événement qui a provoqué une interruption.

Le diviseur, en deux parties de 8 bits, est la valeur par laquelle est divisée la fréquence d'un quartz ou d'une horloge externe pour obtenir la vitesse de transmission.

Sur un certain nombre de processeurs, c'est de cette façon exactement que sont câblés les circuits d'E/S. Écrire ou lire leurs registres se fera par des accès mémoire ordinaires.

Intel se distingue : il existe une patte particulière, IO/M, qui signale un accès aux E/S. Cette patte servira à valider les coupleurs au travers d'une patte CS. Dans le cas d'un accès en E/S, les 16 premiers bits du bus d'adresses seront utilisables. C'est donc un espace mémoire de 64 Ko qui est mis à la disposition des coupleurs. Au niveau logiciel, deux instructions, IN et OUT , sont spécifiquement dévolues à ces accès.

L'avantage de cette solution n'est pas prépondérant pour le programmeur, encore que le fait que le code soit naturellement plus lisible est indiscutablement un bon point. Le circuit de décodage d'adresses se verra simplifié, grâce à l'utilisation de IO/M.

Remarque

Le DMA

Il est possible de demander au processeur d’entrer dans son cocon, c'est-à-dire de mettre une grande partie de ses pattes à l'état haute impédance. Pendant ce temps, un dispositif externe, comme un périphérique rapide, peut accéder directement à la mémoire. Ce dispositif est l'accès direct à la mémoire, DMA .

Le dispositif peut effectuer des transferts périphérie-mémoire, mémoire-périphérie et mémoire-mémoire. Le programmeur aura accès à cette technique par la programmation des coupleurs DMA, comme le 8237A.

1.9 Le démarrage

Le démarrage d'une carte à microprocesseur correspond le plus souvent à sa mise sous tension. Il faut savoir que l'apparition des différentes tensions d'alimentation peut se faire un peu n'importe comment, en fonction de la classe de qualité du matériel. Certains périphériques demandent eux-mêmes à s'initialiser avant de rendre les services attendus. Généralement, un circuit spécialisé est donc prévu. Il est muni d'une résistance et d'un condensateur, dont le rôle est de fabriquer un signal de RESET  propre, avec un retard significatif par rapport à la mise sous tension.

Certains registres ont une valeur précise à l'allumage ; ils sont initialisés par le signal RESET. Un seul registre doit absolument l'être : c'est le compteur programme (IP chez Intel), qui pointe vers la prochaine instruction à exécuter, donc au moment du RESET la première.

Une bonne solution consiste à le charger d'une adresse très près du sommet de la mémoire. À cet endroit, appelé zone de bootstrap, il faut que le processeur trouve quelques lignes de programme, très peu puisque nous sommes presque collés au sommet de cette mémoire. Il est donc quasi obligatoire d'y trouver une instruction de saut. La zone bootstrap et la cible de ce premier saut sont nécessairement dans une ROM, puisqu'elle doit avoir un contenu à la mise sous tension.

Dans d'autres cas, le processeur aura simplement besoin de trouver à un endroit précis une adresse à charger dans le compteur programme. Il s'agit dans ce cas réellement d'un vecteur RESET.

Une fois le programme en ROM démarré, il va initialiser et tester à tour de bras, d'abord le microprocesseur, puis les différents périphériques. Il va faire l'inventaire des options installées, de la quantité de mémoire disponible, voire du type exact de processeur. Il va en résulter des variables système initialisées qui, par exemple, permettront à d'autres programmes d'accéder facilement à ces renseignements de configuration.

Une fois un éventuel périphérique d'affichage initialisé, un message d'accueil pourra être proposé. À la fin de cette phase, un programme d'application pourra être lancé.

Selon le type de carte, le programme lancé pourra être en ROM ou issu d'une mémoire de masse comme une disquette ou un disque dur, et copié en RAM pour y être exécuté. Pour améliorer les performances de vitesse d'accès, la ROM est en réalité souvent copiée en RAM.

1.10 Les interruptions

Nous sommes maintenant en mesure de regarder sans frémir le schéma-bloc d'une carte à microprocesseur à peu près fonctionnelle. Nous commenterons ce schéma en terminant par quelques indications sur les interruptions, dont nous avons déjà parlé sans explication.

Une carte à microprocesseur
figure 1.16 Une carte à microprocesseur [the .swf]

Le microprocesseur, dont très peu de pattes sont représentées, est un modèle 8 bits doté d'un bus AD0 AD9 multiplexé, à la manière du 8086. Contrairement à celui-ci, il adresse les entrées/sorties comme de la mémoire ordinaire. Les capacités mémoire ne sont pas tout à fait réalistes, il faudrait les multiplier par 8 ou 16 pour la RAM et la ROM. À titre indicatif, souvenez-vous que les premiers interpréteurs BASIC en ROM tenaient dans 2 ou 4 Ko, et parmi eux celui écrit par Paul Allen et un certain Bill Gates, BASIC à l'origine de la société Microsoft.

Remarque

Microprocesseurs et microcontrôleurs

Un microcontrôleur est un microprocesseur embarquant de la mémoire souvent, des E/S toujours. La carte représentée ici pourrait très bien être intégrée dans un tel composant. Il suffirait d'extérioriser le quartz, la résistance et le condensateur. Diverses options sont généralement proposées pour la programmation des ROM internes et la possibilité de mémoires externes.

Les clients mémoire sont 5 circuits facilement identifiables. Résumons leurs besoins :

Besoins mémoire de la carte

Nom

Fonction

Besoins mémoire

Bits

ROM Système

Démarrage et fonctions de couplage avec le matériel

64 octets

6

ROM Basic

Interpréteur Basic

256 octets

8

RAM

Mémoire volatile

128 octets

7

Coupleur multi I/O

Entrées/Sorties tout ou rien (TOR). Usages multiples

16 registres en lecture et 16 en écriture

4

Coupleur série

Pour une liaison bidirectionnelle type RS232 vers un terminal

8 registres en lecture et 8 en écriture

3

Le bus devient bus d'adresses à droite du circuit latch. Celui-ci mémorise les 10 bits pour la durée du cycle, à un instant matérialisé par un front sur la patte ALE.

Un nombre suffisant de bits d'adresse, à partir du LSB, est distribué à chaque client mémoire. Quelques circuits logiques sont nommés logique de décodage d'adresse . Ils reçoivent dans notre schéma tous les bits d'adresse pour élaborer 5 signaux, chacun sélectionnant un boîtier en lui affectant une zone mémoire. Ce décodage n'a certainement pas besoin de tous les bits d'adresse pour accomplir son travail. En effet, 4 fils, voire 3, suffisent, mais il faut alors accepter que certains boîtiers possèdent plusieurs adresses et soient situés à plusieurs emplacements en mémoire, comme s'ils étaient autant de circuits différents.

Le bus non verrouillé porte le nom de bus de données . Lui sont ajoutés les deux signaux RD et WR, (read et write), le minimum pour former un bus de contrôle. Le bus de données ne portera des données valides que pendant la validité d'un des signaux du bus de contrôle. Chacun de ces signaux sera simplement câblé sur son équivalent sur chaque circuit. Seul RD sera utilisé pour les ROM.

Un circuit particulier fabrique le signal d'horloge à partir d'un quartz. Ce même circuit, qui peut être intégré ou non, met en forme un signal de RESET issu d'un bouton poussoir. De plus, il génère un RESET propre à la mise sous tension, à l'aide d'une cellule RC, c'est-à-dire d'une résistance et d'un condensateur. Enfin, nous avons ajouté un chien de garde ou watchdog , dont l'utilité évoquée au chapitre suivant est de relancer le système en cas de plantage.

Le coupleur série permet de connecter la carte à un terminal de saisie/affichage, qui pourrait très bien être un PC, ou pourquoi pas un Minitel. Le coupleur multi I/O permet de connecter un certain nombre de périphériques qui demandent des E/S tout ou rien ( TOR ).

Nous constatons la présence sur les coupleurs d'E/S de sorties int (pour interruption), dirigées via une logique adéquate, en fait inutile ici, vers l'entrée  IRQ (interruption request) du processeur.

Parmi les informations arrivant de l'extérieur sur le coupleur multi I/O , figure CP, comme "café prêt". Nous souhaitons boire le café tant qu'il est chaud. Le raisonnement serait le même pour l'arrivée d'un octet sur la liaison série, qu'il faut impérativement lire avant l'arrivée du suivant. Il faut boire les octets tant qu'ils sont chauds.

Allons-nous dérouter périodiquement le flux de notre programme pour venir lire les registres d'E/S, effectuer une scrutation ? Nous le pourrions, mais cette méthode présente ici un certain nombre d'inconvénients majeurs.

Nous allons donc configurer le coupleur pour que, dès l'arrivée de l'information CP, un signal soit levé sur la sortie  int . Que va faire le processeur face à un tel événement ?

Il va terminer l'instruction qu'il était en train de traiter. Ensuite, les valeurs importantes pour la suite du déroulement du programme, le contenu des registres en particulier, vont être rapidement décalqués dans une zone particulière de la mémoire, d'où l'opération inverse sera aisée.

Le compteur programme (dont la valeur actuelle fait partie des registres sauvegardés) va être chargé d'une valeur particulière, le vecteur d'interruption , comparable au vecteur RESET.

À partir de cette adresse, le programmeur aura prévu un bout de code qui va par exemple éteindre la cafetière et signaler à l'écran que le café est prêt ou récupérer l'octet arrivé et le ranger dans un tampon mémoire. Ce sous-programme devra être réduit au strict nécessaire. Il est intuitivement évident que le temps de traitement d'une interruption doit être "très petit" par rapport à celui séparant la levée de deux interruptions, quelle qu'en soient les origines. Ce code de gestion d'interruption se termine par une instruction spécifique de retour d'interruption, qui va récupérer les registres sauvegardés vers leur emplacement d'origine. Cela est en particulier vrai pour le compteur programme, ce qui va permettre au programme de continuer comme si rien ne s'était passé. Quoi que…

Remarque

Données volatiles

Les données susceptibles d'être modifiées par le traitement des interruptions sont qualifiées de volatiles . Le programme ne peut pas considérer comme certaine la persistance de leur valeur entre deux modifications ou lectures par lui-même. Cette notion se retrouvera dans le cadre d'un système d'exploitation multitâche, multiprocesseur, etc. En assembleur, c'est le programmeur qui décidera de chaque lecture et écriture en mémoire, il devra éventuellement tenir compte dans son travail de la possible mise en cache des données volatiles.

En C ou C++, le problème est un peu différent , le compilateur étant susceptible d'optimiser le code. Ainsi :

int main()
{
  int c = 12;
  c = c;
  c = c;
  std::cout << c << std::endl;
  return 0;
}

Ce code génèrera logiquement à la compilation le même code machine que:

int main()
{
  std::cout << (int)12 << std::endl;
  return 0;
}

 Les normes C et C++ stipulent qu'une variable déclarée volatile peut voir sa valeur modifiée à tout moment indépendamment du programme, mais également que lecture et écriture peuvent induire tout type d'effet de bord. En clair, un bon compilateur devra, pour les variables déclarées volatile, suivre à la lettre le code source. C'est le cas de Microsoft C++ 7.1 comme le montre ce listing assembleur :

; 7    :   volatile int c = 12;
    mov    DWORD PTR _c$[esp+4], 12        ; 0000000cH
; 8    :   c = c;
    mov    eax, DWORD PTR _c$[esp+4]
    mov    DWORD PTR _c$[esp+4], eax
; 9    :   c = c;
    mov    ecx, DWORD PTR _c$[esp+4]
    mov    DWORD PTR _c$[esp+4], ecx
; 10   :   std::cout << c << std::endl;

 

Il existe une autre entrée d'interruption, NMI  ( non maskable interrupt ). Ce qui semble indiquer que IRQ  est masquable. En effet, le processeur peut demander à ne pas être dérangé. C'est ce que le programmeur fera souvent pendant une partie critique du code, par exemple avant d'échanger la valeur de deux variables volatiles. Il faudra ensuite voir comment gérer les interruptions arrivées pendant la période de masquage. Tout comme il faudra préciser comment le processeur détermine l'origine de l'interruption quand, cas le plus fréquent, deux ou plus sont possibles. Sachez qu'il existe des circuits intégrés gestionnaires d'interruptions.

La rapidité de réaction est souvent citée pour justifier les interruptions. C'est une explication incomplète, sinon erronée. La véritable raison est qu'il est conceptuellement difficile de placer ces tests d'E/S dans le déroulement d'un code un tant soit peu complexe. D'un bel organigramme, nous passons à une usine à gaz, avec toujours le risque de laisser une branche du programme sans test des E/S et de rencontrer des conditions qui font que le programme reste plus longtemps que prévu dans cette branche. Il n'y a pas réellement de bonne solution hormis les interruptions pour traiter de l'asynchrone à bas niveau. La solution consiste donc à utiliser les interruptions, en ayant délégué le maximum de travail au coupleur d'E/S. En informatique, les capteurs intelligents et communicants apportent confort et sécurité.

La programmation des interruptions matérielles impose certaines précautions spécifiques. Il faudra veiller au rythme des requêtes, combiné à leur temps de traitement. Ce type de programmation fait penser à la programmation sous Windows, programmation par événements. Le noyau de Windows gère la répartition du temps entre les tâches, en utilisant des interruptions pour reprendre la main. Il traite toutes les interruptions, et les transforme si nécessaire en messages adressés à la tâche concernée, c'est à dire qu'il donne la main au gestionnaire de message de la tâche de la tâche.

Il existe également sur certains processeurs (ceux qui nous intéressent en particulier) des interruptions logicielles. Mais cela fait partie de l'étude d'un processeur spécifique et de son jeu d'instructions.

Remarque

La loi de Moore

Gordon Moore présidait la société Intel vers le milieu des années soixante. Il avait constaté que la complexité des puces électroniques, en gros leur intégration ou nombre de fonctions par circuit, doublait pour chaque incrément de temps assez précis, entre une et deux années. Dans le même temps, le coût de cette puce était divisé par deux. Depuis une bonne trentaine d'années, cette affirmation s'est globalement confirmée, mais il ne s'agit que d'une constatation à posteriori, et en aucun cas d'une loi.

Cette période, qui n'est pas encore terminée, correspond plutôt au temps nécessaire pour tirer toute la quintessence de la technologie silicium. Nous pouvons remarquer que cette évolution s'auto-alimente, en ce sens que la présence massive de calculateurs dans les bureaux d'études est nécessaire à la conception et à la mise en fabrication de la génération suivante. Il est raisonnable de penser qu'en 2004 nous sommes plus près de la fin que du début de cette évolution, la suite de l'aventure pouvant passer par un changement radical de technologie.

Nous connaissons maintenant l'architecture matérielle très générale d'un micro-ordinateur et les principes de son fonctionnement, au niveau le plus bas. Nous en connaissons du moins les éléments. Ces bases s'appliquent à tout ce qui est ou ressemble à un micro-ordinateur, passé ou actuel. Citons, de façon non exhaustive : un ancien Atari ou Amiga, différents modèles de stations graphiques, la gamme Apple actuelle, tout ce qui fait aujourd'hui partie du monde PC, selon l'expression courante. Nous pourrions y ajouter, pourquoi pas, un Minitel, une console de jeu ou un terminal de paiement.

Notre ambition est de développer en assembleur dans "le monde PC". La machine que nous utilisons aujourd'hui est issue en droite ligne de l'IBM PC historique, avec lequel elle est presque entièrement compatible. C'est à cette célébrité que nous allons nous intéresser au début du chapitre suivant, plus spécifiquement dans l'optique de sa programmation.

Envoyer un message à l'auteur

se connecter pour pouvoir écrire

Ce site est créé et maintenu (laborieusement) par Pierre Maurette
hébergé par 1 & 1