Wikilivres
frwikibooks
https://fr.wikibooks.org/wiki/Accueil
MediaWiki 1.46.0-wmf.22
first-letter
Média
Spécial
Discussion
Utilisateur
Discussion utilisateur
Wikilivres
Discussion Wikilivres
Fichier
Discussion fichier
MediaWiki
Discussion MediaWiki
Modèle
Discussion modèle
Aide
Discussion aide
Catégorie
Discussion catégorie
Transwiki
Discussion Transwiki
Wikijunior
Discussion Wikijunior
TimedText
TimedText talk
Module
Discussion module
Event
Event talk
Fonctionnement d'un ordinateur/L'adressage des périphériques
0
65771
762961
762569
2026-04-05T13:19:01Z
Mewtow
31375
/* Les coprocesseurs d'entrée-sortie sur les bus partagés */ Déplacement autre chapitre
762961
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu que les périphériques, leurs registres d’interface et leurs contrôleurs, ont chacun une adresse bien précise. Nous avions vu comment le contrôleur de périphérique adresse les périphériques et comment les contrôleurs de périphériques eux-mêmes ont des adresses. Mais nous n'avons pas vu comment le processeur utilise ces adresses.
==Rappels : l'espace d'adressage unifié ou séparé==
Comment s'opère le mélange entre adresses mémoires et adresses de périphérique ? Comment le processeur évite les confusions entre adresses de périphériques et adresses mémoire. Pour cela, il y a plusieurs manières. La plus simple revient à séparer les adresses mémoire et les adresses périphériques, qui ne sont pas transmises sur les mêmes bus. L'autre méthode revient à utiliser un seul ensemble d'adresse, certaines étant allouées à la mémoire, d'autres aux périphériques. Les deux techniques portent des noms assez clairs : l''''espace d'adressage séparé''' pour la première, l''''espace d'adressage unifié''' pour la seconde. Voyons dans le détail ces deux techniques.
===L'espace d’adressage séparé===
Avec la première technique, mémoire et entrées-sorties sont adressées séparément, comme illustré dans le schéma ci-dessous. La mémoire et les entrées-sorties ont chacune un ensemble d'adresse, qui commence à 0 et va jusqu’à une adresse maximale. On dit que la mémoire et les entrées-sorties ont chacune leur propre espace d'adressage.
[[File:Espaces d'adressages séparés entre mémoire et périphérique.png|centre|vignette|upright=2|Espaces d'adressages séparés entre mémoire et périphérique.]]
Avec cette technique, le processeur doit avoir des instructions séparées pour gérer les périphériques et adresser la mémoire. Il a des instructions de lecture/écriture pour lire/écrire en mémoire, et d'autres pour lire/écrire les registres d’interfaçage. L'existence de ces instructions séparées permet de faire la différence entre mémoire et périphérique. Sans cela, le processeur ne saurait pas si une adresse est destinée à un périphérique ou à la mémoire.
===Les entrées-sorties mappées en mémoire===
La seconde technique s'appelle l'espace d'adressage unifie, ou encore les '''entrées-sorties mappées en mémoire'''. Avec cette technique, certaines adresses mémoires sont redirigées automatiquement vers les périphériques. Le périphérique se retrouve inclus dans l'ensemble des adresses utilisées pour manipuler la mémoire : on dit qu'il est mappé en mémoire.
[[File:IO mappées en mémoire.png|centre|vignette|upright=2.0|IO mappées en mémoire]]
L'avantage de cette méthode est la simplicité pour les programmeurs. Il n'y a pas besoin d'instructions différentes pour accéder aux périphériques et à la mémoire. Tout peut être fait par une seule instruction, qui n'a pas besoin de positionner un quelconque bit IO qui n'existe plus. Le processeur possède donc un nombre plus limité d'instructions machines, et est donc plus simple à fabriquer. Mais surtout, les programmeurs peuvent accéder aux périphériques beaucoup plus simplement, en lisant ou écrivant directement dans certaines adresses associées aux périphériques. Les transferts entre mémoire et périphériques sont fortement simplifiés, par exemple.
==Les bus unifiés et les bus d'entrée-sortie==
Maintenant que nous venons de rappeler ce que sont les espaces d'adressage unifiés et séparés, il est temps de voir comment ils sont implémentés en matériel. Et pour cela, nous allons encore une fois faire un rappel sur les bus.
Nous avons vu dans les chapitres précédents qu'il existe en gros trois configurations de base pour les bus.
La première est celle du '''bus système''', un bus unique qui relie la mémoire RAM, la mémoire ROM, le processeur, et les entrées-sorties.
[[File:Bus unique avec entrées mappées en mémoire.png|centre|vignette|upright=2.0|Bus unique avec entrées mappées en mémoire.]]
La seconde est celle d'un bus séparé pour les entrées-sorties, appelé le '''bus d'entrée-sortie'''. Il est en théorie possible d'avoir un bus séparé par controleur de périphérique ou entrée-sortie, mais le nombre de broches utilisé devient rapidement important, ce qui fait que cette technique n'est jamais implémentée.
[[File:Bus d'entrées-sorties multiplexé.png|centre|vignette|upright=2|Bus entre processeur et contrôleur de périphérique.]]
La troisième intercale un '''circuit répartiteur''' entre le processeur et les deux bus. Il s'occupe alors de la gestion des adresses.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===L'implémentation d'un bus IO dédié avec un multiplexeur d'IO===
Les anciens ordinateurs des années 80-90 utilisaient un mélange des techniques 2 et 3 présentées juste au-dessus. Les processeurs avaient un bus d'entrée-sortie séparé des autres, et notamment séparé du bus mémoire. Mais ce bus était connecté à un répartiteur spécialisé dans les IO, qui s'occupait uniquement des entrées-sorties. Les répartiteurs étaient nombreux à l'époque et étaient appelés des circuits de '''''parallel IO''''', bien que ce terme signifie autre chose de nos jours. Les plus connus sont le 8255 d'Intel, le Motorola 6820 PIA (Peripheral Interface Adapter), le WDC 65C21, le MOS Technology 6522 et le MOS Technology CIA.
Pour simplifier les explications, le circuit répartiteur sera appelé un '''multiplexeur d'entrées-sorties''' ou encore un IO MUX. En effet, c'est fondamentalement un multiplexeur/démultiplexeur amélioré. Pour simplifier, un IO MUX dispose de plusieurs ports d'entrée-sortie, un pour le processeur et les autres pour les contrôleurs de périphérique. La liaison point à point entre le CPU et l'IOMUX se faisait sur des broches dédiées, regroupées dans le '''port CPU''', ou port ''processeur''. L'IO MUX avait plusieurs '''ports IO''', ou ports d'entrées-sorties, sur lesquels on connectait un contrôleur de périphérique via une liaison point à point.
[[File:IO MUX.png|centre|vignette|upright=2|IO MUX]]
[[File:8255.svg|vignette|8255]]
Un exemple est celui du 8255, qui disposait de trois ports IO et d'un port CPU. Le port CPU est un port de 8 bits, qui correspond aux broches D0 à D7. Les ports IO sont des ports de 8 bits et sont appelés les ports A, B et C. Leurs broches sont respectivement les broches PA0 à PA7 pour le port A, les broches PB0 à PB7 pour le port B, les broches PC0 à PC7 pour le port C.
: Le 8255 était plus complexe que ce qui est décrit. Le port C servait soit de port IO proprement dit, soit regroupait les bits de contrôle des ports A et B, à savoir les bits de contrôle pour les interruptions et le ''handshaking''. Le 8255 avait aussi plusieurs modes de fonctionnement où les ports IO étaient configurés différemment, le choix du mode étant fait en configurant un registre de contrôle interne au 8255. Le registre de contrôle était adressé via les lignes 10 et A1 qu'on verra plus bas.
Les ports CPU et IO pouvaient fonctionner comme entrée ou sortie et changeaient de rôle suivant la situation, suivant que le CPU pouvait émettre des données en direction d'un périphérique, ou en recevoir. L'IO MUX fonctionnait soit comme un multiplexeur, soit comme un démultiplexeur. Lorsque le processeur envoyait une donnée vers un périphérique, il fonctionnait en démultiplexeur, pour envoyer la donnée vers le bon périphérique, le bon port. En réception, il fonctionnait en multiplexeur et choisissait quel port était connecté au port CPU, quel port envoyait ses données vers le CPU.
Le choix entre multiplexage et démultiplexage se faisait selon que le processeur voulait faire une lecture ou une écriture. Le choix entre les deux était donc le fait d'une entrée de l'IO MUX, l'entrée R/W, qui indiquait s'il fallait faire une lecture ou une écriture.
Qui dit multiplexage/démultiplexage dit : choisir le port IO à connecter au port CPU. Pour cela, les ports étaient numérotés et le CPU pouvait préciser le numéro du port voulu. Et le numéro du port voulu était présenté sur une entrée dédiée, comme sur un MUX ou DEMUX normal. En soit, ce numéro est équivalent à une adresse de périphérique/port, ce qui fait que cette entrée était en réalité un bus d'adresse, appartenant au port CPU. Sur le 8255, l'envoi de l'adresse se faisait sur les deux broches A0 et A1, qui codaient un numéro de 2 bits. Les valeurs étaient les suivantes : 00 = port A, 01 = port B, 10 = port C, 11 = registre de contrôle.
Mais l'IO MUX n'est pas qu'un simple MUX/DEMUX configurable. Il pouvait ''générer des signaux d'interruption''. Quand un périphérique envoyait une donnée à l'IO MUX, il générait un signal d'interruption pour prévenir le processeur qu'une IO a envoyé une donnée. De plus, le répartiteur pouvait ''mettre en attente les données dans des registres'', qui servaient de registres d’interfaçage. Par exemple, une donnée lue sur un port IO était mémorisée dans le répartiteur en attendant que le processeur la récupère. Et inversement, le processeur pouvait envoyer une donnée à un périphérique par l'intermédiaire d'un registre dans le répartiteur. Il écrivait dans ce registre, la donnée était mise en attente dedans en attendant que le périphérique soit libre, et le répartiteur envoyait la donnée quand ce dernier était libéré.
[[File:MOS6526.svg|vignette|MOS 6526.]]
Il faut noter que les ports IO peuvent être aussi bien série que parallèle. Le 8255 avait trois ports IO de 8 bits, qui sont donc tous les trois des ports parallèles. Mais il a existé des IO MUX disposant de deux ports parallèles et un port série. Tel est le cas du MOS Technology 6522 et de son successeur, le MOS Technology CIA. C'était des IO MUX utilisés dans les ordinateurs Commodore, l'Apple III, et quelques autres ordinateurs anciens renommés.
Ils disposaient de deux ports parallèles de 8 bits (PA0-7, PB0-7), chacun ayant 4 lignes de contrôles à leur disposition pour les interruptions, et d'un port série (CB1 et CB2). Le port série était connecté à un registre à décalage de 8 bits, ce qui lui permettait d'envoyer/recevoir un octet à la fois. Ils intégraient aussi des ''timers'' de 16 bits, ainsi qu'une ''Real Time Clock'' pour gérer l'heure.
===Le contrôleur DMA sur un bus partagé ou avec un répartiteur===
L'usage d'un bus dédié a des avantages et des désavantages. L'un des désavantages est lié à l'implémentation du ''Direct Memory Access''. Un bus dédié aux IO marche assez mal avec le DMA. Les échanges entre mémoire RAM et IO doivent passer par le processeur, vu que le bus mémoire est séparé du bus des IO et que le seul point de contact entre les deux est le CPU. En clair : impossible d'utiliser le ''Direct Memory Access''.
À l'opposé, un contrôleur DMA est très adapté à un système avec un bus système, un bus partagé entre CPU, RAM et IO. Le controleur DMA est alors connecté au bus et il se réserve l'accès au bus quand il effectue un transfert DMA.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
L'usage d'un répartiteur ne pose pas de problèmes particuliers pour implémenter le DMA. La seule contrainte est que le contrôleur DMA soit intégré dans le répartiteur. Les échanges entre IO et mémoire passent par le répartiteur, qui fait le pont entre bus mémoire et bus des IO.
[[File:Implémentation du DMA avec un répartiteur.png|centre|vignette|upright=2|Implémentation du DMA avec un répartiteur]]
==L'implémentation matérielle de l'espace d'adressage séparé/unifié==
Intuitivement, on se dit que le bus système va de concert avec un espace d'adressage unifié, avec des entrées-sorties mappées en mémoire. De même, utiliser un bus séparé pour les entrées-sorties va de pair avec des espaces d'adressage séparés. Et dans les grandes lignes, c'est autant vrai que faux.
Un bus système peut implémenter les deux solutions d'adressage, tout dépend de comment on gère le décodage d'adresse (voir plus bas). Il en est de même que la solution avec un répartiteur, tout dépend de comment le répartiteur gère l'espace d'adressage. Par contre, deux bus séparés implique forcément un espace d'adressage séparé. Dans le sens inverse, un espace d'adressage séparé peut être réalisé par toutes les configurations, alors que les entrées-sorties mappées en mémoire impliquent forcément un bus système et/ou un répartiteur.
{|class="wikitable"
|-
!
! Espace d'adressage unifié (entrées-sorties mappées en mémoire)
! Espace d'adressage séparé
|-
! Bus système
| rowspan="2" colspan="2" | Possible, dépend du décodage d'adresse utilisé
|-
! Bus séparé avec répartiteur
|-
! Bus séparé pour les IO
| Non, sauf exceptions
| Oui, obligatoire
|}
Il est possible d'utiliser des configurations intermédiaires, qui permettent d'implémenter des espaces d'adressages séparés ou unifiés. Mais nous verrons cela dans ce qui suit.
===Les entrées-sorties mappées en mémoire avec un bus système===
Dans son implémentation la plus simple, les entrées-sorties mappées en mémoire utilisent un bus système, un bus unique pour les mémoires et les contrôleurs de périphériques. L'avantage est que cela économise beaucoup de fils, sans compter que le bit IO disparait. Par contre, impossible d'accéder à la fois à la mémoire et à un contrôleur d'entrées-sorties en parallèle.
Le principe des entrées-sorties mappées en mémoire est qu'une partie des adresses pointe vers un périphérique, d'autres vers la RAM ou la ROM. L'important est que le bon composant réponde lors d'un accès mémoire/périphérique. Si on accède à une adresse attribuée à la RAM, la RAM doit répondre, les périphériques doivent ignorer l'accès. Et inversement pour un accès périphérique.
La redirection vers le bon destinataire est faite par décodage partiel d'adresse. Pour rappel, chaque périphérique/mémoire possède une entrée CS, qui connecte ou déconnecte le composant du bus. Le circuit de décodage d'adresse prend en entrée l'adresse et commande les bits CS pour désactiver les composants non-concernés et activer la destination. Le circuit de décodage partiel d'adresse va ainsi placer le bit CS de la mémoire à 1 pour les adresses invalidées, l’empêchant de répondre à ces adresses.
[[File:Décodage d'adresse avec entrées-sorties mappées en mémoire.png|centre|vignette|upright=2.0|Décodage d'adresse avec entrées-sorties mappées en mémoire.]]
Le principe est de connecter la mémoire et les entrées-sorties sur le bus système. Le bus d'adresse est connecté à la fois sur la mémoire RAM, sur la mémoire ROM, et sur les entrées-sorties (si elles ont une entrée d'adresse). Le bus de données est lui aussi connecté aux mémoires et aux entrée-sorties. Le décodeur d'adresse est lui relié aux entrées CS de tous ces composants.
[[File:Chipselectfr.png|centre|vignette|upright=1.5|Exemple détaillé.]]
===L'espace d'adressage séparé avec un bus système===
Il est possible d'implémenter l'espace d'adressage séparé sans recourir à des bus séparés. Toutes les configurations de bus possibles sont compatibles avec un espace d'adressage séparé pour les IO, même un bus système unique. Mais comment faire pour l'implémenter avec un bus système ? Là encore, on utilise un système de décodage partiel d'adresse, mais qui est simplifié par rapport à celui des entrée-sorties mappées en mémoire.
Le décodage d'adresse part du principe que le bit de poids fort de l'adresse indique si l'adresse est celle d'un périphérique ou d'une mémoire. Le bit de poids fort de l'adresse, appelé le '''bit I/O''', est mis à 0 pour une adresse mémoire, 1 pour un registre d’interfaçage. Tout cela est réalisé par l'instruction adéquate : une instruction d'accès mémoire positionnera ce bit à 0, alors qu'une instruction d'accès IO le positionnera à 1. L'adresse envoyée sur le bus est formée en récupérant l'adresse à lire/écrire et en positionnant le bit I/O à sa bonne valeur.
Un défaut de cette solution est qu'elle impose d'avoir deux espaces d'adressage de même taille, un pour la/les mémoires, un autre pour les périphériques. Pas question d'avoir un espace d'adressage plus petit pour les périphériques, alors que ce serait possible avec deux bus séparés.
[[File:Bit IO.png|centre|vignette|upright=2|Bit IO.]]
Un avantage de cette méthode est qu'elle marche avec des configurations de bus un peu spéciales, qui sont intermédiaire entre des bus séparés et un bus système. Par exemple, il est possible d'avoir un bus d'adresse partagé, mais pas les autres. Ou encore, il est possible de mutualiser le bus d'adresse et de données, en conservant deux bus de commandes, un pour le périphérique et un pour la mémoire. Le bit IO fonctionne avec toutes ces configurations, la seule contrainte est que le bus d'adresse soit partagé. Mais le processeur doit gérer correctement le bus de données et envoyer les données sur le bon bus de données.
[[File:Espace d'adressage séparé.png|centre|vignette|upright=2|Espace d'adressage séparé.]]
===Les entrées-sorties mappées en mémoire avec des configurations de bus spéciales===
Il est possible d'implémenter les entrées-sorties mappées en mémoire sans utiliser un bus unique, avec des configurations de bus assez spéciales, dans lesquelles on a bien deux bus séparés, mais qui communiquent entre eux. Elles sont très rares, et nous en parlons ici par pur but d'exhaustivité.
La première, de loin la plus simple, consiste à accéder à la RAM d'abord, puis aux périphériques si elle ne répond pas. Une tentative d'accès en RAM fonctionnera du premier coup si l'adresse en question est attribuée à la RAM. Mais si l'adresse est associée à un périphérique, la RAM ne répondra pas et on doit retenter l'accès sur le bus pour les périphériques. L'implémentation est cependant compliquée, sans compter que les performances sont alors réduites, du fait des deux tentatives consécutives.
Les autres solutions font communiquer les deux bus pour que la RAM ou les périphériques détectent précocement les accès qui leur sont dédiés. La première solution de ce type consiste à ajouter un dispositif qui transmet les accès du bus mémoire vers le bus des périphériques. Mais le bus pour les périphériques est souvent moins rapide que le bus mémoire et l'adaptation des vitesses pose des problèmes.
[[File:Espace d'adressage séparé, implémentation avec deux bus séparés.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus]]
{{AutoCat}}
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les méthodes de synchronisation entre processeur et périphériques
| prevText=Les méthodes de synchronisation entre processeur et périphériques
| next=Les périphériques et les cartes d'extension
| nextText=Les périphériques et les cartes d'extension
}}
</noinclude>
gwnrimfuzgzq08k13ntwblm2pbr4wxm
Électricité/Les circuits RL, RC, LC et RLC
0
70984
762989
703436
2026-04-05T17:04:57Z
~2026-20668-34
123412
mot en trop
762989
wikitext
text/x-wiki
<noinclude>{{Électricité}}</noinclude>
Dans ce chapitre, nous poursuivons dans la lignée du chapitre précédent, où nous avons vu des circuits construits avec deux composants parmi les résistances, condensateurs et bobines. Mais dans ce chapitre, nous allons voir des circuits construits avec deux composants différents, là où le chapitre précédent étudiait des circuits avec deux composants identiques. Ces circuits sont connus sous les noms de circuits RC, RL, LC et RLC (avec trois composants, pour ce dernier). Le nom de ces circuits donne les composants du circuit : R symbolise une résistance, L une bobine et C un condensateur.
L'étude de ces circuits en régime continu n'a pas grand intérêt, vu que le condensateur se comporte comme un interrupteur ouvert et les bobines comme des fils électriques (interrupteurs fermés). L'intérêt de ces circuits est d'étudier comment ils se comportent quand on les soumet à un '''échelon de tension''', autrement dit lorsqu'on fait brusquement passer la tension à leur borne de 0 à une valeur U. Même chose pour les échelons de courant. Le circuit va alors mettre un certain temps avant d'atteindre un nouvel équilibre : on dit qu'il est en régime transitoire. Durant ce régime transitoire, les condensateurs et bobines ne se comportent plus comme des interrupteurs. Ce n'est qu’après un certain temps que le circuit atteint un nouvel équilibre et qu'on peut l'analyser comme s'il était en régime permanent, dans un état d'équilibre stable. Dans ce qui va suivre, nous allons analyser certains circuits assez connus, dont le régime transitoire est assez intéressant.
==Les circuits RC==
Le '''circuit RC''' est composé d'une résistance, d'un condensateur et d'un générateur. Le générateur est le plus souvent à un courant continu, mais on peut aussi étudier le cas où il est à courant alternatif. Il existe deux versions du circuit RC : celui où la résistance et le condensateur sont placés en série, et celui où ils sont placés en parallèle. Les deux cas sont illustrés ci-dessous et nous allons les étudier en commençant par le circuit série.
{|
|[[Fichier:RC Series Filter (with V&I Labels).svg|vignette|Circuit RC série.]]
|[[Fichier:Parallel-RC int.svg|vignette|upright=1.1|Circuit RC parallèle.]]
|}
Le circuit RC parallèle n'est pas intéressant à étudier avec des tensions ou courants continus. D'après la loi des mailles, la tension aux bornes du condensateur est égale à la tension de la source/générateur. La variation de cette tension étant nulle, celui-ci se charge très rapidement et aucun courant ne passe plus dans le condensateur. Tout le courant passe dans la résistance, ce qui rend ce circuit inutile. Le seul circuit RC à avoir le moindre intérêt est de loin le circuit RC série.
{|
|[[Fichier:Cr glied.svg|vignette|Notations utilisées dans la suite de la section.]]
|[[Fichier:Condensateur parfait - convention de charge.png|centre|vignette|Condensateur parfait - convention de charge.]]
|[[Fichier:Condensateur parfait - convention de décharge.png|centre|vignette|Condensateur parfait - convention de décharge.]]
|}
===La charge d'un condensateur===
[[Fichier:AC Source-R-C-KCL-KVL.svg|vignette|Maille du circuit RC série.]]
Dans cette section, nous allons étudier ce circuit et regarder ce qui se passe quand on soumet le montage à une tension continue. Le condensateur peut alors se remplir de charges ou se vider. Nous allons d'abord étudier le cas de la charge d'un condensateur, avant de passer à sa décharge. Nous laissons le cas d'une tension ou d'un courant de charge alternatif pour les chapitres ultérieurs, ceux-ci demandant des outils mathématiques assez complexes.
Dans ce qui va suivre, le circuit RC est soudainement connecté à une tension constante <math>U</math>. On commence l'étude du circuit au moment où la tension apparaît, en supposant que le condensateur est initialement vide.
====Les équations====
Vu qu'il s'agit d'un circuit série, la loi des nœuds nous donne :
: <math>i(t) = i_c(t) = i_r(t)</math>
On peut aussi remarquer qu'il n'y a qu'une seule maille dans le circuit RC, ce qui rend son analyse assez simple. La loi des mailles nous donne l'équation suivante :
: <math>U = u_c(t) + u_r(t)</math>
On peut calculer la tension aux bornes de la résistance avec la loi d'Ohm :
: <math>U = u_c(t) + R \cdot i_r(t)</math>
Le courant dans la résistance est égal à celui dans le condensateur (voir supra), donc :
: <math>U = u_c(t) + R \cdot i_c(t)</math>
On applique alors la loi <math>I = C \cdot \frac{dU}{dt}</math> pour calculer le courant <math>i_c(t)</math>.
: <math>U = u_c(t) + R C \cdot \frac{d u_c(t)}{dt}</math>
Quelques manipulations algébriques nous donnent alors l'équation différentielle du premier ordre suivante :
: <math>\frac{d u_c(t)}{dt} + \frac{u_c(t)}{RC} - \frac{U}{RC} = 0</math>
Les mathématiques nous disent que la solution d'une telle équation différentielle est obligatoirement de la forme suivante :
: <math>u_c(t) = A \cdot e^{- \frac{t}{\tau}} + B</math>
Trouver les constantes <math>A</math> et <math>B</math> demande peu de réflexion.
* Après un temps infini, le premier terme sera nul (il tend vers zéro quand t tend vers l'infini). En conséquence, on aura : <math>u_c(t) = B</math>. Or, après un temps infini, la tension aux bornes du condensateur est celle d'un condensateur totalement chargé : ce n'est autre que la tension du générateur <math>U</math>.
* Quand t=0, on a simplement <math>u_c(t) = 0</math>. Dans ce cas, on a : <math>0 = A \cdot e^{- \frac{t}{\tau}} + B = A + B</math>. Donc, <math>A = - B</math>, ce qui donne : <math>A = - U</math>.
L'équation devient donc :
: <math>u_c(t) = U - U \cdot e^{- \frac{t}{\tau}}</math>
: <math>u_c(t) = U \left( 1 - e^{- \frac{t}{\tau}} \right)</math>
Quelques développements supplémentaires nous disent que <math>\tau = R \cdot C</math>, ce qui donne :
: <math>u_c(t) = U \left( 1 - e^{-\frac{t}{RC}} \right)</math>
====L'évolution dans le temps====
[[Fichier:Series RC capacitor voltage.svg|vignette|Tension aux bornes d'un condensateur en charge.]]
Si on dessine le graphe de l'équation précédente, on obtient la figure située à votre droite. On voit que la tension augmente progressivement. On voit aussi que la croissance n'est pas linéaire et que la tension tend vers la tension <math>U</math>, sans pour autant l'atteindre (sauf après un temps infini).
Une chose importante est que le produit <math>\tau = RC</math> est une valeur qui gouverne la croissance de la tension. Dans le détail, il s'agit d'une valeur appelée la '''constante de temps''' du circuit RC. En effet, le produit RC a la dimension d'un temps. Les équations à base d'unités le montrent assez bien :
: <math>R \cdot C = \frac{U}{I} \cdot \frac{Q}{U} = \frac{Q}{I} = T</math>
La figure de droite montre que le condensateur est rempli :
* à 63% au bout d'un temps égal à <math>\tau</math> ;
* à 86.5% au bout d'un temps égal à <math>2 \cdot \tau</math> ;
* à 95% au bout d'un temps égal à <math>3 \cdot \tau</math> ;
* à 98.2% au bout d'un temps égal à <math>4 \cdot \tau</math> ;
* à 99.3% au bout d'un temps égal à <math>5 \cdot \tau</math>.
Les valeurs de 63, 95 et 99% sont de loin les plus importantes à retenir.
===La décharge d'un condensateur===
Après avoir vu la charge d'un condensateur, voyons sa décharge. Celle-ci est similaire à la charge, à quelques différences près. Déjà, on peut décharger un condensateur sans avoir besoin d'un générateur dans le circuit : il suffit de connecter les deux bornes du condensateur entre elles, à travers une résistance. En faisant cela, les charges négatives sur l'armature paire vont rejoindre les charges positives sur l'autre armature. Elles vont alors s'annuler, donnant un condensateur totalement déchargé à la fin du processus. Dans ce qui va suivre, les deux bornes du condensateur sont soudainement connectées entre elles, avec une résistance intercalée entre les deux.
====Les équations====
Vu qu'il s'agit d'un circuit série, la loi des nœuds nous donne :
: <math>i(t) = i_c(t) = i_r(t)</math>
On peut aussi remarquer qu'il n'y a qu'une seule maille dans le circuit RC, ce qui rend son analyse assez simple. La loi des mailles nous donne l'équation suivante :
: <math>u_c(t) = u_r(t)</math>
On peut calculer la tension aux bornes de la résistance avec la loi d'Ohm :
: <math>u_c(t) = R \cdot i_r(t)</math>
Le courant dans la résistance est égal à celui dans le condensateur (voir supra), donc :
: <math>u_c(t) = R \cdot i_c(t)</math>
On applique alors la loi <math>I = C \cdot \frac{dU}{dt}</math> pour calculer le courant <math>i_c(t)</math>.
: <math>u_c(t) = R C \cdot \frac{d u_c(t)}{dt}</math>
Quelques manipulations algébriques nous donnent alors l'équation différentielle du premier ordre suivante :
: <math>\frac{d u_c(t)}{dt} + \frac{u_c(t)}{RC} = 0</math>
Sa solution est de la forme :
: <math display="inline">u_c(t) = K \cdot e^{\frac{-t}{\tau}}</math>
En se rappelant de la condition initiale <math>u_c(0) = U</math>, on trouve : <math>K = U</math>. On a alors :
: <math display="inline">u_c(t) = U \cdot e^{\frac{-t}{\tau}}</math>
====L'évolution dans le temps====
[[Fichier:Series RC resistor voltage.svg|vignette|Tension aux bornes d'un condensateur en cours de décharge.]]
Si on dessine le graphe de l'équation précédente, on obtient la figure située à votre droite. On voit que la tension diminue progressivement. On voit aussi que la décroissance n'est pas linéaire et que la tension tend vers zéro, sans pour autant l'atteindre (sauf après un temps infini). La figure de droite montre que le condensateur est déchargé :
* à 36.8% au bout d'un temps égal à <math>\tau</math> ;
* à 13.5% au bout d'un temps égal à <math>2 \cdot \tau</math> ;
* à 5% au bout d'un temps égal à <math>3 \cdot \tau</math> ;
* à 1.8% au bout d'un temps égal à <math>4 \cdot \tau</math> ;
* à 0.7% au bout d'un temps égal à <math>5 \cdot \tau</math>.
Les valeurs de 37, 5 et 1% sont de loin les plus importantes à retenir.
==Circuit RL==
Les '''circuits RL''' sont similaires aux circuits RC, si ce n'est que le condensateur est remplacé par une bobine. Ils sont composés d'une résistance placée en série ou en parallèle d'une bobine. Le circuit avec la bobine en parallèle de la résistance n'est pas intéressant en courant continu. La bobine n'étant pas autre chose qu'un fil électrique en courant continu, la résistance est simplement court-circuitée. Même chose pour le circuit série, une fois que le courant est stabilisé : il est équivalent avec une résistance connectée directement au générateur. Mais le circuit RL est intéressant à étudier quand le régime permanent (stable) n'est pas encore atteint.
{|
|[[Fichier:RL Series Filter (with V&I Labels).svg|vignette|Circuit RL série.]]
|[[Fichier:RL Parallel Filter (with I Labels).svg|vignette|Circuit RL parallèle.]]
|}
===Circuit RL série===
[[Fichier:RL Series Open-Closed.svg|vignette|Circuit RL étudié lors de sa charge.]]
Étudions maintenant le '''circuit RL série'''
Vu qu'il s'agit d'un circuit série, la loi des nœuds nous donne :
: <math>I = i_l(t) = i_r(t)</math>
On peut aussi remarquer qu'il n'y a qu'une seule maille dans le circuit, ce qui fait que la loi des mailles donne :
: <math>U = u_l(t) + u_r(t)</math>
On peut calculer la tension aux bornes de la résistance avec la loi d'Ohm et celle aux bornes de la bobine par la loi <math>U_l = L \cdot \frac{dI}{dt}</math>. On obtient l'équation différentielle suivante :
: <math>L \frac{dI}{dt} + R \cdot I = U</math>
Les mathématiques nous disent que la solution d'une telle équation différentielle est la suivante, avec <math>\tau = \frac{L}{R}</math> :
: <math>I = \frac{U}{R} \left( 1 - e^{- \frac{t}{\tau}} \right)</math>
On voit que cette équation est identique à celle de la charge d'un condensateur, si ce n'est que la valeur de la constante de temps change et que c'est l'intensité du courant dans la bobine qui est donnée par l'équation (et non la tension). On peut donc reprendre les figures et graphiques obtenus dans la section sur le circuit RC et les appliquer au circuit RL sans problème.
===Circuit RL parallèle===
Dans le '''circuit RL parallèle''', on a deux mailles : une qui passe par la résistance et une autre par la bobine. On a donc :
: <math>u_l(t) - u_r(t) = 0</math>
En utilisant les lois <math>U = R \cdot I</math> et <math>U = L \cdot \frac{dI}{dt}</math>, on obtient :
: <math>L \cdot \frac{dI(t)}{dt} - R \cdot I(t) = 0</math>
Les mathématiques nous disent que la solution d'une telle équation différentielle est la suivante, avec <math>\tau = \frac{L}{R}</math> :
: <math>I = \frac{U}{R} \cdot e^{\frac{-t}{\tau}}</math>
Cette équation est identique à celle de la décharge d'un condensateur, si ce n'est que la valeur de la constante de temps change et que c'est l'intensité du courant dans la bobine qui est donnée par l'équation (et non la tension). On peut donc reprendre les figures et graphiques obtenus dans la section sur le circuit RC et les appliquer au circuit RL sans problème.
==Circuit LC==
Les '''circuits LC''' combinent un condensateur et une bobine, qui sont placés soit en série soit en parallèle.
{|
|[[Fichier:Series LC Circuit.svg|vignette|Circuit LC série.]]
|[[Fichier:Parallel LC Circuit.svg|vignette|upright=1.5|Circuit LC parallèle.]]
|}
===Circuit LC série===
[[Fichier:LC serie.JPG|vignette|Circuit LC.]]
Le circuit série est illustré dans la figure de gauche. D'après la loi des mailles, on a :
: <math>U - (U_L + U_C) = 0</math>
En utilisant la formule <math>U_L = L \cdot \frac{dI}{dt}</math>, on a :
: <math>U - L \cdot \frac{dI}{dt} - U_C = 0</math>
D'après la loi des nœuds, le courant est le même dans tous les récepteurs, et est donc égal au courant qui circule dans le condensateur. On peut alors utiliser la formule <math>I = C \cdot \frac{dU_C}{dt}</math> pour calculer le courant et sa dérivée, ce qui donne :
: <math>U - LC \cdot \frac{d^2 U_C}{dt^2} - U_C = 0</math>
[[File:LC-circuit01.svg|vignette|Illustration de la tension et du courant dans un circuit LC.]]
Les connaisseurs, qui ont eu une formation en physique, auront remarqué que l'équation est celle d'un oscillateur harmonique et savent ce qui va venir dans la suite. La solution de cette équation différentielle est, d'après les mathématiques, une fonction sinusoïdale :
: <math>u_c = E \cdot A \cdot \cos{\left( \frac{t}{\sqrt{LC}} + \varphi \right)}</math>
On voit que la tension aux bornes du condensateur est une tension alternative sinusoïdale. Il en est de même aux bornes de la bobine, à cause de la loi des mailles. La tension et le courant sont illustrés dans la figure de droite. La tension sinusoïdale a une fréquence et une période égales à :
: <math>T = 2 \pi \sqrt{LC}</math>
: <math>f = \frac{1}{2 \pi \sqrt{LC}}</math>
[[Fichier:Tuned circuit animation 3.gif|centre|vignette|Illustration du courant dans un circuit LC.]]
==Circuit RLC==
Les '''circuits RLC''' combinent une résistance, un condensateur et une bobine, qui sont placées soit en série soit en parallèle. Il existe différentes possibilités pour le circuit RLC :
* soit les trois récepteurs sont en série ;
* soit les trois sont en parallèle ;
* soit deux récepteurs sont en série et l'autre en parallèle :
** la résistance est en parallèle, la bobine et le condensateur en série ;
** la bobine est en parallèle, la résistance et le condensateur en série ;
** le condensateur est en parallèle, la bobine et la résistance en série.
{|
|[[Fichier:RLC series circuit v1.svg|vignette|upright=0.5|Circuit RLC série.]]
|[[Fichier:RLC parallel circuit v1.svg|vignette|upright=1.3|Circuit RLC parallèle.]]
|}
===Circuit RLC série===
[[Fichier:MFrey RLC Series Circuit.svg|vignette|upright=0.5|Circuit RLC.]]
Le circuit série est illustré dans la figure de gauche. D'après la loi des mailles, on a :
: <math>U - (U_R + U_L + U_C) = 0</math>
En utilisant les formules <math>U_R = R \cdot I</math> et <math>U_L = L \cdot \frac{dI}{dt}</math>, on a :
: <math>U - L \cdot \frac{dI}{dt} - R \cdot I - U_C = 0</math>
D'après la loi des nœuds, le courant est le même dans tous les récepteurs, et est donc égal au courant qui circule dans le condensateur. On peut alors utiliser la formule <math>I = C \cdot \frac{dU}{dt}</math> pour calculer le courant et sa dérivée, ce qui donne :
: <math>LC \cdot \frac{d^2 U_C}{dt^2} + RC \cdot \frac{dU_C}{dt} + U_C = U</math>
Si R = 0, le circuit se simplifie en un vulgaire circuit LC comme celui étudié précédemment. Par contre, si R n'est pas nulle, le terme <math>RC \cdot \frac{dU_C}{dt}</math> agit comme une force de frottement qui amortit les oscillations. Les connaisseurs qui ont eu une formation en physique, auront remarqué que l'équation est celle d'un oscillateur harmonique amorti. De tels systèmes physiques ont une évolution similaire à celle donnée dans la figure ci-dessous. On voit que les oscillations sont progressivement atténuées et voient leur amplitude réduite progressivement.
[[Fichier:RLC transient plot.svg|centre|vignette|upright=2.0|Oscillations d'un circuit RLC.]]
efod80zuzpmfohd9mbri8scps5ntjax
Fonctionnement d'un ordinateur/Les méthodes de synchronisation entre processeur et périphériques
0
78309
762962
762688
2026-04-05T13:19:03Z
Mewtow
31375
/* Un exemple de chipset pour le bus ISA */
762962
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Il a existé des contrôleurs DMA améliorés, qui étaient rendus programmables. De tels contrôleurs DMA améliorés étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur périphérique'''.
De tels coprocesseurs périphériques permettent de décharger le processeur principal, le CPU (''Central Processing Unit''), de tout ce qui a trait aux entrées-sorties. Il est moins performant en calcul que le processeur principal, l'accent étant mis sur les accès mémoire. Sauf erreur, ils ne sont présents que sur les systèmes avec un bus partagé, il n'en existe pas pour les systèmes avec un répartiteur ou uns IO dédié.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs périphériques exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme périphérique'''. Il y a généralement un programme périphérique pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes périphériques sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur .
Avec un coprocesseur périphérique, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur périphérique, en lui fournissant un pointeur qui pointe vers le programme périphérique (l'adresse du programme). Suite à cette instruction, le coprocesseur périphérique exécute son programme dans son coin, séparément du processeur. Quand le programme périphérique s'est terminé, le coprocesseur périphérique envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
Dans le cas général, le programme émule les transferts de données entre RAM et périphériques, qui sont autrement gérés par des transferts DMA. Pour cela, les coprocesseurs périphériques peuvent émuler le DMA avec un programme spécialisé qui copie les mots mémoire un par un avec une boucle. Le coprocesseur périphérique incorpore pour cela plusieurs registres spécialisés : des registres contenant des pointeurs, des registres pour des indices de boucle ou des compteurs. Les registres pointeurs sont généralement incrémentés ou décrémentés, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction du coprocesseur périphérique est conçu pour implémenter de telles boucles. Il peut exécuter des instructions assez diverses : branchements, instructions d'accès mémoire, additions/soustractions. Ses instructions se concentrent surtout sur les accès mémoire et laissent de côté les instructions de calcul. Il ne dispose souvent que d'instructions d'addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées.
La connexion entre le coprocesseur périphérique et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur périphérique et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs périphériques sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs IO sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
1afg6clq1oznia4ei79a3jctpnmg8kv
762963
762962
2026-04-05T13:21:09Z
Mewtow
31375
/* Les coprocesseurs d'entrée-sorties */
762963
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
De tels coprocesseurs I/O permettent de décharger le processeur principal, le CPU (''Central Processing Unit''), de tout ce qui a trait aux entrées-sorties. Ils sont moins performant en calcul que le processeur principal, l'accent étant mis sur les accès mémoire. Sauf erreur, ils ne sont présents que sur les systèmes avec un bus partagé, il n'en existe pas pour les systèmes avec un répartiteur ou uns IO dédié.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme périphérique'''. Il y a généralement un programme périphérique pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes périphériques sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur .
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur périphérique, en lui fournissant un pointeur qui pointe vers le programme périphérique (l'adresse du programme). Suite à cette instruction, le coprocesseur périphérique exécute son programme dans son coin, séparément du processeur. Quand le programme périphérique s'est terminé, le coprocesseur périphérique envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
Dans le cas général, le programme émule les transferts de données entre RAM et périphériques, qui sont autrement gérés par des transferts DMA. Pour cela, les coprocesseurs périphériques peuvent émuler le DMA avec un programme spécialisé qui copie les mots mémoire un par un avec une boucle. Le coprocesseur périphérique incorpore pour cela plusieurs registres spécialisés : des registres contenant des pointeurs, des registres pour des indices de boucle ou des compteurs. Les registres pointeurs sont généralement incrémentés ou décrémentés, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction du coprocesseur I/O est conçu pour implémenter de telles boucles. Il peut exécuter des instructions assez diverses : branchements, instructions d'accès mémoire, additions/soustractions. Ses instructions se concentrent surtout sur les accès mémoire et laissent de côté les instructions de calcul. Il ne dispose souvent que d'instructions d'addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées.
La connexion entre le coprocesseur périphérique et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur périphérique et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs périphériques sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs IO sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
gw1yt8gj6jndm6dmbbgpefikscle9xd
762964
762963
2026-04-05T13:24:59Z
Mewtow
31375
/* Les coprocesseurs d'entrée-sorties */
762964
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
: Sauf erreur, ils ne sont présents que sur les systèmes avec un bus partagé, il n'en existe pas pour les systèmes avec un répartiteur ou uns IO dédié.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
[[File:Intel 8089.svg|vignette|Intel 8089]]
Les coprocesseurs IO sont assez rares, leur utilisation est peu fréquent. Ils étaient présents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Il a existé un coprocesseur IO vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli.
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme périphérique'''. Il y a généralement un programme périphérique pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes périphériques sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur .
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur périphérique, en lui fournissant un pointeur qui pointe vers le programme périphérique (l'adresse du programme). Suite à cette instruction, le coprocesseur périphérique exécute son programme dans son coin, séparément du processeur. Quand le programme périphérique s'est terminé, le coprocesseur périphérique envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
Dans le cas général, le programme émule les transferts de données entre RAM et périphériques, qui sont autrement gérés par des transferts DMA. Pour cela, les coprocesseurs périphériques peuvent émuler le DMA avec un programme spécialisé qui copie les mots mémoire un par un avec une boucle. Le coprocesseur périphérique incorpore pour cela plusieurs registres spécialisés : des registres contenant des pointeurs, des registres pour des indices de boucle ou des compteurs. Les registres pointeurs sont généralement incrémentés ou décrémentés, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction du coprocesseur I/O est conçu pour implémenter de telles boucles. Il peut exécuter des instructions assez diverses : branchements, instructions d'accès mémoire, additions/soustractions. Ses instructions se concentrent surtout sur les accès mémoire et laissent de côté les instructions de calcul. Il ne dispose souvent que d'instructions d'addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées.
La connexion entre le coprocesseur périphérique et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur périphérique et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs périphériques sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs IO sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
2tz5t9zubqs8gtpkxbjd2e13bw9timz
762965
762964
2026-04-05T13:26:19Z
Mewtow
31375
/* Les coprocesseurs d'entrée-sorties */
762965
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
[[File:Intel 8089.svg|vignette|Intel 8089]]
Les coprocesseurs IO sont assez rares, leur utilisation est peu fréquent. Ils étaient présents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Il a existé un coprocesseur IO vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. L'Intel 8089 était utilisé de concert avec un CPU Intel 8086, les deux étaient reliés au bus système, partagé avec les entrées-sorties. A l'opposé, les ''Channel IO'' des anciens ''mainframes'' n'étaient pas reliés à un bus système, mais à un bus d'entrée-sortie séparé.
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme périphérique'''. Il y a généralement un programme périphérique pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes périphériques sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur .
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur périphérique, en lui fournissant un pointeur qui pointe vers le programme périphérique (l'adresse du programme). Suite à cette instruction, le coprocesseur périphérique exécute son programme dans son coin, séparément du processeur. Quand le programme périphérique s'est terminé, le coprocesseur périphérique envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
Dans le cas général, le programme émule les transferts de données entre RAM et périphériques, qui sont autrement gérés par des transferts DMA. Pour cela, les coprocesseurs périphériques peuvent émuler le DMA avec un programme spécialisé qui copie les mots mémoire un par un avec une boucle. Le coprocesseur périphérique incorpore pour cela plusieurs registres spécialisés : des registres contenant des pointeurs, des registres pour des indices de boucle ou des compteurs. Les registres pointeurs sont généralement incrémentés ou décrémentés, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction du coprocesseur I/O est conçu pour implémenter de telles boucles. Il peut exécuter des instructions assez diverses : branchements, instructions d'accès mémoire, additions/soustractions. Ses instructions se concentrent surtout sur les accès mémoire et laissent de côté les instructions de calcul. Il ne dispose souvent que d'instructions d'addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées.
La connexion entre le coprocesseur périphérique et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur périphérique et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs périphériques sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs IO sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
3finipn9jnqy9720yolk6lezi69os9y
762966
762965
2026-04-05T13:52:28Z
Mewtow
31375
/* Les coprocesseurs d'entrée-sorties */
762966
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
[[File:Intel 8089.svg|vignette|Intel 8089]]
Les coprocesseurs I/O sont assez rares, leur utilisation est peu fréquent. Ils étaient présents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur .
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O t d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions. Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecter le processeur au coprocesseur I/O. Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Un avantage de la seconde solution est que le coprocesseur I/O peut disposer de sa propre mémoire RAM. Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au corprocesseur, pour le programme IO.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|thumb|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur I/O, en lui fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Suite à cette instruction, le coprocesseur IO exécute son programme dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
Dans le cas général, le programme émule les transferts de données entre RAM et périphériques, qui sont autrement gérés par des transferts DMA. Pour cela, les coprocesseurs IO peuvent émuler le DMA avec un programme spécialisé qui copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela plusieurs registres spécialisés : des registres contenant des pointeurs, des registres pour des indices de boucle ou des compteurs. Les registres pointeurs sont généralement incrémentés ou décrémentés, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction du coprocesseur I/O est conçu pour implémenter de telles boucles. Il peut exécuter des instructions assez diverses : branchements, instructions d'accès mémoire, additions/soustractions. Ses instructions se concentrent surtout sur les accès mémoire et laissent de côté les instructions de calcul. Il ne dispose souvent que d'instructions d'addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées.
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
7pgy51qclgj7aq3t4l1klrc7rfim826
762967
762966
2026-04-05T13:54:16Z
Mewtow
31375
/* Les coprocesseurs d'entrée-sorties */
762967
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
[[File:Intel 8089.svg|vignette|Intel 8089]]
Les coprocesseurs I/O sont assez rares, leur utilisation est peu fréquent. Ils étaient présents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur .
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O t d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions. Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecter le processeur au coprocesseur I/O. Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Un avantage de la seconde solution est que le coprocesseur I/O peut disposer de sa propre mémoire RAM. Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au corprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|thumb|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur I/O, en lui fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Suite à cette instruction, le coprocesseur IO exécute son programme dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
Dans le cas général, le programme émule les transferts de données entre RAM et périphériques, qui sont autrement gérés par des transferts DMA. Pour cela, les coprocesseurs IO peuvent émuler le DMA avec un programme spécialisé qui copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela plusieurs registres spécialisés : des registres contenant des pointeurs, des registres pour des indices de boucle ou des compteurs. Les registres pointeurs sont généralement incrémentés ou décrémentés, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction du coprocesseur I/O est conçu pour implémenter de telles boucles. Il peut exécuter des instructions assez diverses : branchements, instructions d'accès mémoire, additions/soustractions. Ses instructions se concentrent surtout sur les accès mémoire et laissent de côté les instructions de calcul. Il ne dispose souvent que d'instructions d'addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées.
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
d506ni9jz8pkhpqb540cyfu6gagdpnc
762968
762967
2026-04-05T13:56:10Z
Mewtow
31375
/* Les coprocesseurs d'entrée-sorties */
762968
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur.c
[[File:Intel 8089.svg|vignette|Intel 8089]]
Les coprocesseurs I/O sont assez rares, leur utilisation est peu fréquent. Ils étaient présents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O t d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions. Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecter le processeur au coprocesseur I/O. Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Un avantage de la seconde solution est que le coprocesseur I/O peut disposer de sa propre mémoire RAM. Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au corprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
===Le jeu d'instruction du coprocesseur IO===
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur I/O, en lui fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Suite à cette instruction, le coprocesseur IO exécute son programme dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
Dans le cas général, le programme émule les transferts de données entre RAM et périphériques, qui sont autrement gérés par des transferts DMA. Pour cela, les coprocesseurs IO peuvent émuler le DMA avec un programme spécialisé qui copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela plusieurs registres spécialisés : des registres contenant des pointeurs, des registres pour des indices de boucle ou des compteurs. Les registres pointeurs sont généralement incrémentés ou décrémentés, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction du coprocesseur I/O est conçu pour implémenter de telles boucles. Il peut exécuter des instructions assez diverses : branchements, instructions d'accès mémoire, additions/soustractions. Ses instructions se concentrent surtout sur les accès mémoire et laissent de côté les instructions de calcul. Il ne dispose souvent que d'instructions d'addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
9e8z7u2s1vsetbzwkscxqn6o15lai9a
762969
762968
2026-04-05T14:00:35Z
Mewtow
31375
/* Les interconnexions entre coprocesseur IO, CPU et entrée-sorties */
762969
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur.c
[[File:Intel 8089.svg|vignette|Intel 8089]]
Les coprocesseurs I/O sont assez rares, leur utilisation est peu fréquent. Ils étaient présents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O t d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions. Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecter le processeur au coprocesseur I/O. Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
===Le jeu d'instruction du coprocesseur IO===
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur I/O, en lui fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Suite à cette instruction, le coprocesseur IO exécute son programme dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
Dans le cas général, le programme émule les transferts de données entre RAM et périphériques, qui sont autrement gérés par des transferts DMA. Pour cela, les coprocesseurs IO peuvent émuler le DMA avec un programme spécialisé qui copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela plusieurs registres spécialisés : des registres contenant des pointeurs, des registres pour des indices de boucle ou des compteurs. Les registres pointeurs sont généralement incrémentés ou décrémentés, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction du coprocesseur I/O est conçu pour implémenter de telles boucles. Il peut exécuter des instructions assez diverses : branchements, instructions d'accès mémoire, additions/soustractions. Ses instructions se concentrent surtout sur les accès mémoire et laissent de côté les instructions de calcul. Il ne dispose souvent que d'instructions d'addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
tqi1y8yei7xt6sx6iicfrk0nn4fi1pq
762970
762969
2026-04-05T14:05:43Z
Mewtow
31375
/* Le jeu d'instruction du coprocesseur IO */
762970
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur.c
[[File:Intel 8089.svg|vignette|Intel 8089]]
Les coprocesseurs I/O sont assez rares, leur utilisation est peu fréquent. Ils étaient présents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O t d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions. Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecter le processeur au coprocesseur I/O. Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
===Le jeu d'instruction du coprocesseur IO===
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur I/O, en lui fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Suite à cette instruction, le coprocesseur IO exécute son programme dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
Dans le cas général, le programme I/O émule les transferts DMA entre RAM et périphériques. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela plusieurs registres spécialisés : des registres contenant des pointeurs, des registres pour des indices de boucle ou des compteurs. Les registres pointeurs sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés.
Le jeu d'instruction du coprocesseur I/O est conçu pour implémenter de telles boucles. Il peut exécuter des instructions assez diverses : branchements, instructions d'accès mémoire, additions/soustractions. Ses instructions se concentrent surtout sur les accès mémoire et laissent de côté les instructions de calcul. Il ne dispose souvent que d'instructions d'addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
acikzx1uattj0uw283kjpnorly8lgog
762971
762970
2026-04-05T14:11:56Z
Mewtow
31375
/* Les interconnexions entre coprocesseur IO, CPU et entrée-sorties */
762971
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur.c
[[File:Intel 8089.svg|vignette|Intel 8089]]
Les coprocesseurs I/O sont assez rares, leur utilisation est peu fréquent. Ils étaient présents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O t d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecte le processeur au coprocesseur I/O. Mais utiliser un bus système a un gros avantage : le coprocesseur I/O peut gérer les transferts DMA !
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
===Le jeu d'instruction du coprocesseur IO===
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur I/O, en lui fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Suite à cette instruction, le coprocesseur IO exécute son programme dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
Dans le cas général, le programme I/O émule les transferts DMA entre RAM et périphériques. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela plusieurs registres spécialisés : des registres contenant des pointeurs, des registres pour des indices de boucle ou des compteurs. Les registres pointeurs sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés.
Le jeu d'instruction du coprocesseur I/O est conçu pour implémenter de telles boucles. Il peut exécuter des instructions assez diverses : branchements, instructions d'accès mémoire, additions/soustractions. Ses instructions se concentrent surtout sur les accès mémoire et laissent de côté les instructions de calcul. Il ne dispose souvent que d'instructions d'addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
3k2dst5pyijgprann94tot4y1mk1rwy
762972
762971
2026-04-05T14:12:39Z
Mewtow
31375
/* Les interconnexions entre coprocesseur IO, CPU et entrée-sorties */
762972
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur.c
[[File:Intel 8089.svg|vignette|Intel 8089]]
Les coprocesseurs I/O sont assez rares, leur utilisation est peu fréquent. Ils étaient présents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. De plus, l'implémentation du DMA est très simple, le coprocesseur IO peut le prendre en charge très facilement. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O sert d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecte le processeur au coprocesseur I/O. Mais utiliser un bus système a un gros avantage : le coprocesseur I/O peut gérer les transferts DMA !
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
===Le jeu d'instruction du coprocesseur IO===
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur I/O, en lui fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Suite à cette instruction, le coprocesseur IO exécute son programme dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
Dans le cas général, le programme I/O émule les transferts DMA entre RAM et périphériques. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela plusieurs registres spécialisés : des registres contenant des pointeurs, des registres pour des indices de boucle ou des compteurs. Les registres pointeurs sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés.
Le jeu d'instruction du coprocesseur I/O est conçu pour implémenter de telles boucles. Il peut exécuter des instructions assez diverses : branchements, instructions d'accès mémoire, additions/soustractions. Ses instructions se concentrent surtout sur les accès mémoire et laissent de côté les instructions de calcul. Il ne dispose souvent que d'instructions d'addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
no989rguna7fol4v4p5psjrsl8od3l5
762973
762972
2026-04-05T14:18:12Z
Mewtow
31375
/* Les coprocesseurs d'entrée-sorties */
762973
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur.c
[[File:Intel 8089.svg|vignette|Intel 8089]]
Les coprocesseurs I/O sont assez rares, leur utilisation est peu fréquent. Ils étaient présents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. De plus, l'implémentation du DMA est très simple, le coprocesseur IO peut le prendre en charge très facilement. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O sert d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecte le processeur au coprocesseur I/O. Mais utiliser un bus système a un gros avantage : le coprocesseur I/O peut gérer les transferts DMA !
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
===Le jeu d'instruction du coprocesseur IO===
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur I/O, en lui fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Suite à cette instruction, le coprocesseur IO exécute son programme dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
Dans le cas général, le programme I/O émule les transferts DMA entre RAM et périphériques. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela plusieurs registres spécialisés : des registres contenant des pointeurs, des registres pour des indices de boucle ou des compteurs. Les registres pointeurs sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés.
Le jeu d'instruction du coprocesseur I/O est conçu pour implémenter de telles boucles. Il peut exécuter des instructions assez diverses : branchements, instructions d'accès mémoire, additions/soustractions. Ses instructions se concentrent surtout sur les accès mémoire et laissent de côté les instructions de calcul. Il ne dispose souvent que d'instructions d'addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeur de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
eqij138qfd9g17fxw38q11r1mo7da7s
762974
762973
2026-04-05T14:20:25Z
Mewtow
31375
/* Le jeu d'instruction du coprocesseur IO */
762974
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur.c
[[File:Intel 8089.svg|vignette|Intel 8089]]
Les coprocesseurs I/O sont assez rares, leur utilisation est peu fréquent. Ils étaient présents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. De plus, l'implémentation du DMA est très simple, le coprocesseur IO peut le prendre en charge très facilement. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O sert d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecte le processeur au coprocesseur I/O. Mais utiliser un bus système a un gros avantage : le coprocesseur I/O peut gérer les transferts DMA !
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
===Le jeu d'instruction du coprocesseur IO===
Dans le cas général, le programme I/O émule les transferts DMA entre RAM et périphériques. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela plusieurs registres spécialisés : des registres contenant des pointeurs, des registres pour des indices de boucle ou des compteurs. Les registres pointeurs sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés.
Le jeu d'instruction du coprocesseur I/O est conçu pour implémenter de telles boucles. Il peut exécuter des instructions assez diverses : branchements, instructions d'accès mémoire, additions/soustractions. Ses instructions se concentrent surtout sur les accès mémoire et laissent de côté les instructions de calcul. Il ne dispose souvent que d'instructions d'addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeur de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur I/O, en lui fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Suite à cette instruction, le coprocesseur IO exécute son programme dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
otwhr9bs3yhvn5xf05613sc4ngd0oc9
762975
762974
2026-04-05T14:22:58Z
Mewtow
31375
/* Le jeu d'instruction du coprocesseur IO */
762975
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur.c
[[File:Intel 8089.svg|vignette|Intel 8089]]
Les coprocesseurs I/O sont assez rares, leur utilisation est peu fréquent. Ils étaient présents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. De plus, l'implémentation du DMA est très simple, le coprocesseur IO peut le prendre en charge très facilement. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O sert d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecte le processeur au coprocesseur I/O. Mais utiliser un bus système a un gros avantage : le coprocesseur I/O peut gérer les transferts DMA !
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction du coprocesseur I/O est conçu pour implémenter de telles boucles. Il peut exécuter des instructions assez diverses : branchements, instructions d'accès mémoire, additions/soustractions. Ses instructions se concentrent surtout sur les accès mémoire et laissent de côté les instructions de calcul. Il ne dispose souvent que d'instructions d'addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeur de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur I/O, en lui fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Suite à cette instruction, le coprocesseur IO exécute son programme dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
gsna046eyalc5oaq1t2r35mzask3xqv
762976
762975
2026-04-05T14:30:54Z
Mewtow
31375
/* Le jeu d'instruction du coprocesseur IO */
762976
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur.c
[[File:Intel 8089.svg|vignette|Intel 8089]]
Les coprocesseurs I/O sont assez rares, leur utilisation est peu fréquent. Ils étaient présents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. De plus, l'implémentation du DMA est très simple, le coprocesseur IO peut le prendre en charge très facilement. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O sert d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecte le processeur au coprocesseur I/O. Mais utiliser un bus système a un gros avantage : le coprocesseur I/O peut gérer les transferts DMA !
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeur de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur I/O, en lui fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Suite à cette instruction, le coprocesseur IO exécute son programme dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
rjxtvwwrti5lwh7l32ql4qd7ktca6gs
762977
762976
2026-04-05T15:37:13Z
Mewtow
31375
/* Les coprocesseurs d'entrée-sorties */
762977
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur.c
Les coprocesseurs I/O étaient très fréquents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Mais pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. De plus, l'implémentation du DMA est très simple, le coprocesseur IO peut le prendre en charge très facilement. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O sert d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecte le processeur au coprocesseur I/O. Mais utiliser un bus système a un gros avantage : le coprocesseur I/O peut gérer les transferts DMA !
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeur de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur I/O, en lui fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Suite à cette instruction, le coprocesseur IO exécute son programme dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve des instructions de copie mémoire, des branchements, des instructions de manipulation de bit, et quelques calculs mineurs.
Programmer le 8089 demande de lui fournir deux choses : un ''Channel Control Block'' et un ''Command Parameter Block''. Le ''Command Parameter Block'' est un programme à exécuter sur le 8089, alors que le ''Channel Control Block'' sert juste à configurer le 8089. Les deux sont placés en mémoire RAM, sur le bus système, et le 8089 lit les deux depuis la RAM. Mais le processeur doit fournir l'adresse de ces deux "programmes" au 8089 quand il l'initialise.
Le ''Channel Control Block'' contient des données de configuration, pour configurer les deux canaux DMA. Pour cela, il contient deux sections identiques, une par canal DMA. Chacune contient : un octet de configuration, un octet BUSY qui indique l'état du canal DMA, et l'adresse programme à exécuter. L'adresse du programme à exécuter est précisément l'adresse du ''Command Parameter Block'', à un détail près : le 8089 et le 8086 utilisent la segmentation. Il y a donc deux adresses : l'adresse du segment, et la position du programme dans ce segment.
{|class="wikitable"
|+ ''Channel Control Block''
|-
! Octet !! Octet
|-
| Octet BUSY || Octet de configuration
|-
| colspan="2" | Adresse dans le segment adressé par la seconde adresse
|-
| colspan="2" | Adresse du segment du ''Command Parameter Block''
|}
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
2d8kqpgatwr9hievfyknp7oohdkt68o
762978
762977
2026-04-05T15:43:14Z
Mewtow
31375
/* L'Intel 8089 */
762978
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur.c
Les coprocesseurs I/O étaient très fréquents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Mais pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. De plus, l'implémentation du DMA est très simple, le coprocesseur IO peut le prendre en charge très facilement. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O sert d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecte le processeur au coprocesseur I/O. Mais utiliser un bus système a un gros avantage : le coprocesseur I/O peut gérer les transferts DMA !
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeur de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur I/O, en lui fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Suite à cette instruction, le coprocesseur IO exécute son programme dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve des instructions de copie mémoire, des branchements, des instructions de manipulation de bit, et quelques calculs mineurs.
Programmer le 8089 demande de lui fournir deux choses : un ''Channel Control Block'' et un ''Command Parameter Block''. Le ''Command Parameter Block'' est un programme à exécuter sur le 8089, alors que le ''Channel Control Block'' sert juste à configurer le 8089. Les deux sont placés en mémoire RAM, sur le bus système, et le 8089 lit les deux depuis la RAM. Mais le processeur doit fournir l'adresse de ces deux "programmes" au 8089 quand il l'initialise.
Le ''Channel Control Block'' contient des données de configuration, pour configurer les deux canaux DMA. Pour cela, il contient deux sections identiques, une par canal DMA. Chacune contient : un octet de configuration, un octet BUSY qui indique l'état du canal DMA, et l'adresse programme à exécuter. L'adresse du programme à exécuter est précisément l'adresse du ''Command Parameter Block'', à un détail près : le 8089 et le 8086 utilisent la segmentation. Il y a donc deux adresses : l'adresse du segment, et la position du programme dans ce segment.
{|class="wikitable"
|+ ''Channel Control Block''
|-
! Octet 1 !! colspan="5" |Octet 2
|-
| rowspan="2" | Octet BUSY
| colspan="4" | Octet de configuration
|-
| START/STOPSUSPEND/CONTINUE || INTERRUPT ENABLE || PRIORITY || ...
|-
| colspan="5" | Adresse dans le segment adressé par la seconde adresse
|-
| colspan="5" | Adresse du segment du ''Command Parameter Block''
|}
L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
ohj9gdam65ybhpw0j94ibf9krfvtpak
762979
762978
2026-04-05T15:48:53Z
Mewtow
31375
/* L'Intel 8089 */
762979
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur.c
Les coprocesseurs I/O étaient très fréquents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Mais pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. De plus, l'implémentation du DMA est très simple, le coprocesseur IO peut le prendre en charge très facilement. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O sert d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecte le processeur au coprocesseur I/O. Mais utiliser un bus système a un gros avantage : le coprocesseur I/O peut gérer les transferts DMA !
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeur de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur I/O, en lui fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Suite à cette instruction, le coprocesseur IO exécute son programme dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve des instructions de copie mémoire, des branchements, des instructions de manipulation de bit, et quelques calculs mineurs.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs : un ''Channel Control Block'' et un ''Command Parameter Block''. Le ''Command Parameter Block'' contient l'adresse du programme à exécuter sur le 8089, alors que le ''Channel Control Block'' sert juste à configurer le 8089. Les deux sont placés en mémoire RAM, sur le bus système, et le 8089 lit les deux depuis la RAM. Mais le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise.
Le ''Channel Control Block'' contient des données de configuration, pour configurer les deux canaux DMA. Pour cela, il contient deux sections identiques, une par canal DMA. Chacune contient : un octet de configuration, un octet BUSY qui indique l'état du canal DMA, et l'adresse programme à exécuter. L'adresse du programme à exécuter est précisément l'adresse du ''Command Parameter Block'', à un détail près : le 8089 et le 8086 utilisent la segmentation. Il y a donc deux adresses : l'adresse du segment, et la position du programme dans ce segment.
{|class="wikitable"
|+ ''Channel Control Block''
|-
! Octet 1 !! colspan="5" |Octet 2
|-
| rowspan="2" | Octet BUSY
| colspan="4" | Octet de configuration
|-
| START/STOPSUSPEND/CONTINUE || INTERRUPT ENABLE || PRIORITY || ...
|-
| colspan="5" | Position du ''Command Parameter Block'' dans le segment adressé
|-
| colspan="5" | Adresse du segment du ''Command Parameter Block''
|}
L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Là encore, elle est composée de deux adresses, à cause de la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
1en09ggjt7rdbj4jolrifn8wz4395rg
762980
762979
2026-04-05T16:02:54Z
Mewtow
31375
/* L'Intel 8089 */
762980
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur.c
Les coprocesseurs I/O étaient très fréquents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Mais pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. De plus, l'implémentation du DMA est très simple, le coprocesseur IO peut le prendre en charge très facilement. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O sert d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecte le processeur au coprocesseur I/O. Mais utiliser un bus système a un gros avantage : le coprocesseur I/O peut gérer les transferts DMA !
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeur de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur I/O, en lui fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Suite à cette instruction, le coprocesseur IO exécute son programme dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve des instructions de copie mémoire, des branchements, des instructions de manipulation de bit, et quelques calculs mineurs.
Le 8089 contient plusieurs registres, avec des registres de 20/21 bits séparés des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Lors d'un transfert DMA, le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination. GC sert quand le programme I/O doit adresser une donnée avec adressage indirect. Ils peuvent tous les trois être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
Le registre TP est le ''program counter'' du canal DMA.
Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Les trois premiers peuvent être utilisés comme registres de données entre deux transferts DMA.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* BC sert à compter le nombre d'octets qu'il reste à transférer, lors d'un transfert DMA.
* MC est prédisposé à gérer un masque, qui est utilisé soit lors des transferts DMA, soit de concert avec les instructions JMCE and JMCNE.
* CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement apr le 8089.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs : un ''Channel Control Block'' et un ''Command Parameter Block''. Le ''Command Parameter Block'' contient l'adresse du programme à exécuter sur le 8089, alors que le ''Channel Control Block'' sert juste à configurer le 8089. Les deux sont placés en mémoire RAM, sur le bus système, et le 8089 lit les deux depuis la RAM. Mais le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise.
Le ''Channel Control Block'' contient des données de configuration, pour configurer les deux canaux DMA. Pour cela, il contient deux sections identiques, une par canal DMA. Chacune contient : un octet de configuration, un octet BUSY qui indique l'état du canal DMA, et l'adresse programme à exécuter. L'adresse du programme à exécuter est précisément l'adresse du ''Command Parameter Block'', à un détail près : le 8089 et le 8086 utilisent la segmentation. Il y a donc deux adresses : l'adresse du segment, et la position du programme dans ce segment.
{|class="wikitable"
|+ ''Channel Control Block''
|-
! Octet 1 !! colspan="5" |Octet 2
|-
| rowspan="2" | Octet BUSY
| colspan="4" | Octet de configuration
|-
| START/STOPSUSPEND/CONTINUE || INTERRUPT ENABLE || PRIORITY || ...
|-
| colspan="5" | Position du ''Command Parameter Block'' dans le segment adressé
|-
| colspan="5" | Adresse du segment du ''Command Parameter Block''
|}
L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Là encore, elle est composée de deux adresses, à cause de la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
rvig19nvxv2oy6okyn3ksz52tdakfbf
762981
762980
2026-04-05T16:13:03Z
Mewtow
31375
/* L'Intel 8089 */
762981
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur.c
Les coprocesseurs I/O étaient très fréquents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Mais pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. De plus, l'implémentation du DMA est très simple, le coprocesseur IO peut le prendre en charge très facilement. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O sert d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecte le processeur au coprocesseur I/O. Mais utiliser un bus système a un gros avantage : le coprocesseur I/O peut gérer les transferts DMA !
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeur de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur I/O, en lui fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Suite à cette instruction, le coprocesseur IO exécute son programme dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve des instructions de copie mémoire, des branchements, des instructions de manipulation de bit, et quelques calculs mineurs.
Le 8089 contient plusieurs registres, avec des registres de 20/21 bits séparés des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Le registre TP est le ''program counter'' associé à un canal DMA. Les deux canaux DMA exécutent chacun leur propre programme, ce qui fait qu'ils ont chacun leur propre ''program counter''. Mais pour le reste, les registres ne font pas la même chose selon qu'un transfert DMA est en cours ou non.
Lors d'un transfert DMA, les registres sont utilisés comme suit :
* Le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination.
* Le registre GC est facultatif : il pointe vers une table de 256 octets utilisé pour faire de la traduction d'adresse.
* Le registre BC compte le nombre d'octets qu'il reste à transférer. Le registre MC mémorise un masque, qui est utilisé pour appliquer une opération de masquage avant d'écrire l'octet dans la destination.
* Le registre CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement par le 8089.
* Les autres registres ne sont pas utilisés.
En-dehors d'un transfert DMA, les registres sont utilisés pour stocker des données, à l'exception de TP et de CC.
* Les trois registres d'adresse GA, GB et GC peuvent être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* MC est prédisposé à gérer un masque, qui est utilisé avec les instructions JMCE and JMCNE.
* Le registre BC n'a pas de prédisposition en-dehors des transferts DMA.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs : un ''Channel Control Block'' et un ''Command Parameter Block''. Le ''Command Parameter Block'' contient l'adresse du programme à exécuter sur le 8089, alors que le ''Channel Control Block'' sert juste à configurer le 8089. Les deux sont placés en mémoire RAM, sur le bus système, et le 8089 lit les deux depuis la RAM. Mais le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise.
Le ''Channel Control Block'' contient des données de configuration, pour configurer les deux canaux DMA. Pour cela, il contient deux sections identiques, une par canal DMA. Chacune contient : un octet de configuration, un octet BUSY qui indique l'état du canal DMA, et l'adresse programme à exécuter. L'adresse du programme à exécuter est précisément l'adresse du ''Command Parameter Block'', à un détail près : le 8089 et le 8086 utilisent la segmentation. Il y a donc deux adresses : l'adresse du segment, et la position du programme dans ce segment.
{|class="wikitable"
|+ ''Channel Control Block''
|-
! Octet 1 !! colspan="5" |Octet 2
|-
| rowspan="2" | Octet BUSY
| colspan="4" | Octet de configuration
|-
| START/STOPSUSPEND/CONTINUE || INTERRUPT ENABLE || PRIORITY || ...
|-
| colspan="5" | Position du ''Command Parameter Block'' dans le segment adressé
|-
| colspan="5" | Adresse du segment du ''Command Parameter Block''
|}
L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Là encore, elle est composée de deux adresses, à cause de la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
m94kr53ensv6ew3r1i54urt7a1ylpa6
762982
762981
2026-04-05T16:16:38Z
Mewtow
31375
/* L'Intel 8089 */
762982
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur.c
Les coprocesseurs I/O étaient très fréquents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Mais pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. De plus, l'implémentation du DMA est très simple, le coprocesseur IO peut le prendre en charge très facilement. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O sert d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecte le processeur au coprocesseur I/O. Mais utiliser un bus système a un gros avantage : le coprocesseur I/O peut gérer les transferts DMA !
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeur de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur I/O, en lui fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Suite à cette instruction, le coprocesseur IO exécute son programme dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve des instructions de copie mémoire, des branchements, des instructions de manipulation de bit, et quelques calculs mineurs. Pour les calculs, il gère l'addition, l'incrémentation, la décrémentation, c'est tout. Les instructions de manipulations de bit regroupe les ET/OU/NON bit à bit, deux instructions pour mettre un bit à 0 ou 1, ainsi que des branchements pour tester la valeur d'un bit dans un registre ou une adresse. Il dispose aussi d'instruction de copie mémoire-mémoire, ainsi que des instructions pour :
* terminer l'exécution du programme I/O ;
* mettre la sortie d’interruption à 1 ;
* configurer un transfert DMA ;
* démarrer un transfert DMA à la prochaine instruction.
Le 8089 contient plusieurs registres, avec des registres de 20/21 bits séparés des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Le registre TP est le ''program counter'' associé à un canal DMA. Les deux canaux DMA exécutent chacun leur propre programme, ce qui fait qu'ils ont chacun leur propre ''program counter''. Mais pour le reste, les registres ne font pas la même chose selon qu'un transfert DMA est en cours ou non.
Lors d'un transfert DMA, les registres sont utilisés comme suit :
* Le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination.
* Le registre GC est facultatif : il pointe vers une table de 256 octets utilisé pour faire de la traduction d'adresse.
* Le registre BC compte le nombre d'octets qu'il reste à transférer. Le registre MC mémorise un masque, qui est utilisé pour appliquer une opération de masquage avant d'écrire l'octet dans la destination.
* Le registre CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement par le 8089.
* Les autres registres ne sont pas utilisés.
En-dehors d'un transfert DMA, les registres sont utilisés pour stocker des données, à l'exception de TP et de CC.
* Les trois registres d'adresse GA, GB et GC peuvent être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* MC est prédisposé à gérer un masque, qui est utilisé avec les instructions JMCE and JMCNE.
* Le registre BC n'a pas de prédisposition en-dehors des transferts DMA.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs : un ''Channel Control Block'' et un ''Command Parameter Block''. Le ''Command Parameter Block'' contient l'adresse du programme à exécuter sur le 8089, alors que le ''Channel Control Block'' sert juste à configurer le 8089. Les deux sont placés en mémoire RAM, sur le bus système, et le 8089 lit les deux depuis la RAM. Mais le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise.
Le ''Channel Control Block'' contient des données de configuration, pour configurer les deux canaux DMA. Pour cela, il contient deux sections identiques, une par canal DMA. Chacune contient : un octet de configuration, un octet BUSY qui indique l'état du canal DMA, et l'adresse programme à exécuter. L'adresse du programme à exécuter est précisément l'adresse du ''Command Parameter Block'', à un détail près : le 8089 et le 8086 utilisent la segmentation. Il y a donc deux adresses : l'adresse du segment, et la position du programme dans ce segment.
{|class="wikitable"
|+ ''Channel Control Block''
|-
! Octet 1 !! colspan="5" |Octet 2
|-
| rowspan="2" | Octet BUSY
| colspan="4" | Octet de configuration
|-
| START/STOPSUSPEND/CONTINUE || INTERRUPT ENABLE || PRIORITY || ...
|-
| colspan="5" | Position du ''Command Parameter Block'' dans le segment adressé
|-
| colspan="5" | Adresse du segment du ''Command Parameter Block''
|}
L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Là encore, elle est composée de deux adresses, à cause de la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
7wuea7zy4a0c7bm4hbmxd7squ4grnyn
762983
762982
2026-04-05T16:28:46Z
Mewtow
31375
/* L'Intel 8089 */
762983
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur.c
Les coprocesseurs I/O étaient très fréquents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Mais pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. De plus, l'implémentation du DMA est très simple, le coprocesseur IO peut le prendre en charge très facilement. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O sert d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecte le processeur au coprocesseur I/O. Mais utiliser un bus système a un gros avantage : le coprocesseur I/O peut gérer les transferts DMA !
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeur de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur I/O, en lui fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Suite à cette instruction, le coprocesseur IO exécute son programme dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve les instructions classiques, avec quelques ajouts dédiés aux transferts DMA. Pour les calculs, il gère l'addition, l'incrémentation, la décrémentation, les ET/OU/NON bit à bit, et c'est tout. Il y a aussi deux instructions pour mettre un bit à 0 ou 1, ainsi que des branchements pour tester la valeur d'un bit dans un registre ou une adresse. Mais les instructions intéressantes sont :
* les instructions de copie mémoire-mémoire ;
* une instruction pour terminer l'exécution du programme I/O ;
* une instruction pour mettre la sortie d’interruption à 1 ;
* une instruction pour configurer un transfert DMA ;
* une instruction pour démarrer un transfert DMA à la prochaine instruction.
Le 8089 contient plusieurs registres, avec des registres de 20/21 bits séparés des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Le registre TP est le ''program counter'' associé à un canal DMA. Les deux canaux DMA exécutent chacun leur propre programme, ce qui fait qu'ils ont chacun leur propre ''program counter''. Mais pour le reste, les registres ne font pas la même chose selon qu'un transfert DMA est en cours ou non.
Lors d'un transfert DMA, les registres sont utilisés comme suit :
* Le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination.
* Le registre GC est facultatif : il pointe vers une table de 256 octets utilisé pour faire de la traduction d'adresse.
* Le registre BC compte le nombre d'octets qu'il reste à transférer. Le registre MC mémorise un masque, qui est utilisé pour appliquer une opération de masquage avant d'écrire l'octet dans la destination.
* Le registre CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement par le 8089.
* Les autres registres ne sont pas utilisés.
En-dehors d'un transfert DMA, les registres sont utilisés pour stocker des données, à l'exception de TP et de CC.
* Les trois registres d'adresse GA, GB et GC peuvent être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* MC est prédisposé à gérer un masque, qui est utilisé avec les instructions JMCE and JMCNE.
* Le registre BC n'a pas de prédisposition en-dehors des transferts DMA.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs : un ''Channel Control Block'' et un ''Command Parameter Block''. Le ''Command Parameter Block'' contient l'adresse du programme à exécuter sur le 8089, alors que le ''Channel Control Block'' sert juste à configurer le 8089. Les deux sont placés en mémoire RAM, sur le bus système, et le 8089 lit les deux depuis la RAM. Mais le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise.
Le ''Channel Control Block'' contient des données de configuration, pour configurer les deux canaux DMA. Pour cela, il contient deux sections identiques, une par canal DMA. Chacune contient : un octet de configuration, un octet BUSY qui indique l'état du canal DMA, et l'adresse programme à exécuter. L'adresse du programme à exécuter est précisément l'adresse du ''Command Parameter Block'', à un détail près : le 8089 et le 8086 utilisent la segmentation. Il y a donc deux adresses : l'adresse du segment, et la position du programme dans ce segment.
{|class="wikitable"
|+ ''Channel Control Block''
|-
! Octet 1 !! colspan="5" |Octet 2
|-
| rowspan="2" | Octet BUSY
| colspan="4" | Octet de configuration
|-
| START/STOPSUSPEND/CONTINUE || INTERRUPT ENABLE || PRIORITY || ...
|-
| colspan="5" | Position du ''Command Parameter Block'' dans le segment adressé
|-
| colspan="5" | Adresse du segment du ''Command Parameter Block''
|}
L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Là encore, elle est composée de deux adresses, à cause de la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
eh686t7xowcxcyzvyqq9bxu48yl1gio
762984
762983
2026-04-05T16:36:37Z
Mewtow
31375
/* L'Intel 8089 */
762984
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur.c
Les coprocesseurs I/O étaient très fréquents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Mais pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. De plus, l'implémentation du DMA est très simple, le coprocesseur IO peut le prendre en charge très facilement. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O sert d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecte le processeur au coprocesseur I/O. Mais utiliser un bus système a un gros avantage : le coprocesseur I/O peut gérer les transferts DMA !
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeur de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur I/O, en lui fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Suite à cette instruction, le coprocesseur IO exécute son programme dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve les instructions classiques, avec quelques ajouts dédiés aux transferts DMA. Les calculs se limitent à l'addition, l'incrémentation, la décrémentation, et les ET/OU/NON bit à bit. Il y a aussi deux instructions pour mettre un bit à 0 ou 1, ainsi que des branchements pour tester la valeur d'un bit dans un registre ou une adresse. Mais les instructions intéressantes sont :
* les instructions de copie mémoire-mémoire ;
* une instruction pour configurer un transfert DMA ;
* une instruction pour démarrer un transfert DMA à la prochaine instruction ;
* une instruction pour terminer l'exécution du programme I/O ;
* une instruction pour mettre la sortie d’interruption à 1.
Le 8089 contient des registres de 20/21 bits et des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses, alors que les registres de 16 bits sont des registres de données. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Le registre TP est le ''program counter'' associé à un canal DMA. Les deux canaux DMA exécutent chacun leur propre programme, ce qui fait qu'ils ont chacun leur propre ''program counter''. Mais pour le reste, les registres ne font pas la même chose selon qu'un transfert DMA est en cours ou non.
Lors d'un transfert DMA, les registres sont utilisés comme suit :
* Le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination.
* Le registre GC est facultatif : il pointe vers une table de 256 octets utilisé pour faire de la traduction d'adresse.
* Le registre BC compte le nombre d'octets qu'il reste à transférer. Le registre MC mémorise un masque, qui est utilisé pour appliquer une opération de masquage avant d'écrire l'octet dans la destination.
* Le registre CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement par le 8089.
* Les autres registres ne sont pas utilisés.
En-dehors d'un transfert DMA, les registres sont utilisés pour stocker des données, à l'exception de TP et de CC.
* Les trois registres d'adresse GA, GB et GC peuvent être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
* Le registre BC n'a pas de prédisposition en-dehors des transferts DMA.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* MC est prédisposé à gérer un masque, qui est utilisé avec les instructions JMCE and JMCNE.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs de mémoire : un ''Channel Control Block'' et un ''Command Parameter Block''. Les deux sont placés en mémoire RAM, sur le bus système, et le 8089 lit les deux depuis la RAM. Le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise. Je ne vais pas donner une explication complète de ces deux blocs, mais je vais quand même donner quelques détails.
Le ''Channel Control Block'' contient, pour chaque canal DMA, un octet de configuration. L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Elle est en réalité composée de deux adresses, parce que le 8089 et le 8086 utilisent la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
bc0pd06o92f74kfdp52hfc9jyb0crx3
762985
762984
2026-04-05T16:39:53Z
Mewtow
31375
/* L'Intel 8089 */
762985
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir und ernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donnerait des routines d'interruptions déportées sur un autre processeur.c
Les coprocesseurs I/O étaient très fréquents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Mais pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. De plus, l'implémentation du DMA est très simple, le coprocesseur IO peut le prendre en charge très facilement. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O sert d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecte le processeur au coprocesseur I/O. Mais utiliser un bus système a un gros avantage : le coprocesseur I/O peut gérer les transferts DMA !
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeur de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur I/O, en lui fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Suite à cette instruction, le coprocesseur IO exécute son programme dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'Intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve les instructions classiques, avec quelques ajouts dédiés aux transferts DMA. Les calculs se limitent à l'addition, l'incrémentation, la décrémentation, et les ET/OU/NON bit à bit. Il y a aussi deux instructions pour mettre un bit à 0 ou 1, ainsi que des branchements pour tester la valeur d'un bit dans un registre ou une adresse. Mais les instructions intéressantes sont :
* les instructions de copie mémoire-mémoire ;
* une instruction pour configurer un transfert DMA ;
* une instruction pour démarrer un transfert DMA à la prochaine instruction ;
* une instruction pour terminer l'exécution du programme I/O ;
* une instruction pour mettre la sortie d’interruption à 1.
Le 8089 contient des registres de 20/21 bits et des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses, alors que les registres de 16 bits sont des registres de données. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Le registre TP est le ''program counter'' associé à un canal DMA. Les deux canaux DMA exécutent chacun leur propre programme, ce qui fait qu'ils ont chacun leur propre ''program counter''. Mais pour le reste, les registres ne font pas la même chose selon qu'un transfert DMA est en cours ou non.
Lors d'un transfert DMA, les registres sont utilisés comme suit :
* Le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination.
* Le registre GC est facultatif : il pointe vers une table de 256 octets utilisé pour faire de la traduction d'adresse.
* Le registre BC compte le nombre d'octets qu'il reste à transférer. Le registre MC mémorise un masque, qui est utilisé pour appliquer une opération de masquage avant d'écrire l'octet dans la destination.
* Le registre CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement par le 8089.
* Les autres registres ne sont pas utilisés.
En-dehors d'un transfert DMA, les registres sont utilisés pour stocker des données, à l'exception de TP et de CC.
* Les trois registres d'adresse GA, GB et GC peuvent être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
* Le registre BC n'a pas de prédisposition en-dehors des transferts DMA.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* MC est prédisposé à gérer un masque, qui est utilisé avec les instructions JMCE and JMCNE.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs de mémoire : un ''Channel Control Block'' et un ''Command Parameter Block''. Les deux sont placés en mémoire RAM, le 8089 y accède en lisant dans la mémoire RAM, et le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise. Je ne vais pas donner une explication complète de ces deux blocs, mais je vais quand même donner quelques détails.
Le ''Channel Control Block'' contient, pour chaque canal DMA, un octet de configuration. L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Elle est en réalité composée de deux adresses, parce que le 8089 et le 8086 utilisent la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
mjjp54aiaggwr2skw20hhmfprtjachh
762986
762985
2026-04-05T16:40:49Z
Mewtow
31375
/* Les coprocesseurs d'entrée-sorties */
762986
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir un dernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donneraient des routines d'interruptions déportées sur un autre processeur.
Les coprocesseurs I/O étaient très fréquents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Mais pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. De plus, l'implémentation du DMA est très simple, le coprocesseur IO peut le prendre en charge très facilement. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O sert d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecte le processeur au coprocesseur I/O. Mais utiliser un bus système a un gros avantage : le coprocesseur I/O peut gérer les transferts DMA !
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeurs de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
Avec un coprocesseur I/O, le CPU ne dispose que d'une seule instruction liées aux entrées-sorties. Elle initie l'exécution d'un programme d'entrée-sortie sur le coprocesseur I/O, en lui fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Suite à cette instruction, le coprocesseur IO exécute son programme dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'Intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve les instructions classiques, avec quelques ajouts dédiés aux transferts DMA. Les calculs se limitent à l'addition, l'incrémentation, la décrémentation, et les ET/OU/NON bit à bit. Il y a aussi deux instructions pour mettre un bit à 0 ou 1, ainsi que des branchements pour tester la valeur d'un bit dans un registre ou une adresse. Mais les instructions intéressantes sont :
* les instructions de copie mémoire-mémoire ;
* une instruction pour configurer un transfert DMA ;
* une instruction pour démarrer un transfert DMA à la prochaine instruction ;
* une instruction pour terminer l'exécution du programme I/O ;
* une instruction pour mettre la sortie d’interruption à 1.
Le 8089 contient des registres de 20/21 bits et des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses, alors que les registres de 16 bits sont des registres de données. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Le registre TP est le ''program counter'' associé à un canal DMA. Les deux canaux DMA exécutent chacun leur propre programme, ce qui fait qu'ils ont chacun leur propre ''program counter''. Mais pour le reste, les registres ne font pas la même chose selon qu'un transfert DMA est en cours ou non.
Lors d'un transfert DMA, les registres sont utilisés comme suit :
* Le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination.
* Le registre GC est facultatif : il pointe vers une table de 256 octets, utilisée pour faire de la traduction d'adresse.
* Le registre BC compte le nombre d'octets qu'il reste à transférer. Le registre MC mémorise un masque, qui est utilisé pour appliquer une opération de masquage avant d'écrire l'octet dans la destination.
* Le registre CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement par le 8089.
* Les autres registres ne sont pas utilisés.
En dehors d'un transfert DMA, les registres sont utilisés pour stocker des données, à l'exception de TP et de CC.
* Les trois registres d'adresse GA, GB et GC peuvent être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
* Le registre BC n'a pas de prédisposition en dehors des transferts DMA.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* MC est prédisposé à gérer un masque, qui est utilisé avec les instructions JMCE and JMCNE.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs de mémoire : un ''Channel Control Block'' et un ''Command Parameter Block''. Les deux sont placés en mémoire RAM, le 8089 y accède en lisant dans la mémoire RAM, et le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise. Je ne vais pas donner une explication complète de ces deux blocs, mais je vais quand même donner quelques détails.
Le ''Channel Control Block'' contient, pour chaque canal DMA, un octet de configuration. L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Elle est en réalité composée de deux adresses, parce que le 8089 et le 8086 utilisent la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
6d4aq8iai59m9gvzqm62vn00q7pv794
762990
762986
2026-04-05T17:05:39Z
Mewtow
31375
/* Le jeu d'instruction du coprocesseur IO */
762990
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir un dernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donneraient des routines d'interruptions déportées sur un autre processeur.
Les coprocesseurs I/O étaient très fréquents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Mais pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. De plus, l'implémentation du DMA est très simple, le coprocesseur IO peut le prendre en charge très facilement. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O sert d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecte le processeur au coprocesseur I/O. Mais utiliser un bus système a un gros avantage : le coprocesseur I/O peut gérer les transferts DMA !
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeurs de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'Intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve les instructions classiques, avec quelques ajouts dédiés aux transferts DMA. Les calculs se limitent à l'addition, l'incrémentation, la décrémentation, et les ET/OU/NON bit à bit. Il y a aussi deux instructions pour mettre un bit à 0 ou 1, ainsi que des branchements pour tester la valeur d'un bit dans un registre ou une adresse. Mais les instructions intéressantes sont :
* les instructions de copie mémoire-mémoire ;
* une instruction pour configurer un transfert DMA ;
* une instruction pour démarrer un transfert DMA à la prochaine instruction ;
* une instruction pour terminer l'exécution du programme I/O ;
* une instruction pour mettre la sortie d’interruption à 1.
Le 8089 contient des registres de 20/21 bits et des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses, alors que les registres de 16 bits sont des registres de données. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Le registre TP est le ''program counter'' associé à un canal DMA. Les deux canaux DMA exécutent chacun leur propre programme, ce qui fait qu'ils ont chacun leur propre ''program counter''. Mais pour le reste, les registres ne font pas la même chose selon qu'un transfert DMA est en cours ou non.
Lors d'un transfert DMA, les registres sont utilisés comme suit :
* Le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination.
* Le registre GC est facultatif : il pointe vers une table de 256 octets, utilisée pour faire de la traduction d'adresse.
* Le registre BC compte le nombre d'octets qu'il reste à transférer. Le registre MC mémorise un masque, qui est utilisé pour appliquer une opération de masquage avant d'écrire l'octet dans la destination.
* Le registre CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement par le 8089.
* Les autres registres ne sont pas utilisés.
En dehors d'un transfert DMA, les registres sont utilisés pour stocker des données, à l'exception de TP et de CC.
* Les trois registres d'adresse GA, GB et GC peuvent être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
* Le registre BC n'a pas de prédisposition en dehors des transferts DMA.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* MC est prédisposé à gérer un masque, qui est utilisé avec les instructions JMCE and JMCNE.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs de mémoire : un ''Channel Control Block'' et un ''Command Parameter Block''. Les deux sont placés en mémoire RAM, le 8089 y accède en lisant dans la mémoire RAM, et le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise. Je ne vais pas donner une explication complète de ces deux blocs, mais je vais quand même donner quelques détails.
Le ''Channel Control Block'' contient, pour chaque canal DMA, un octet de configuration. L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Elle est en réalité composée de deux adresses, parce que le 8089 et le 8086 utilisent la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
dad1r7ozmgd0q6xjwfwht1b7fjka0xn
762991
762990
2026-04-05T17:06:31Z
Mewtow
31375
/* Les coprocesseurs d'entrée-sorties */
762991
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir un dernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donneraient des routines d'interruptions déportées sur un autre processeur.
Avec un coprocesseur I/O, le CPU initie l'exécution d'un programme I/O sur le coprocesseur I/O. Le coprocesseur IO exécute alors le programme I/O dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
Les coprocesseurs I/O étaient très fréquents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Mais pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple. De plus, l'implémentation du DMA est très simple, le coprocesseur IO peut le prendre en charge très facilement. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O sert d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecte le processeur au coprocesseur I/O. Mais utiliser un bus système a un gros avantage : le coprocesseur I/O peut gérer les transferts DMA !
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Les deux se marchent sur les pieds pour accéder à la mémoire RAM. Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à la connecter au coprocesseur I/O.
Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser la seconde méthode, à savoir d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeurs de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'Intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve les instructions classiques, avec quelques ajouts dédiés aux transferts DMA. Les calculs se limitent à l'addition, l'incrémentation, la décrémentation, et les ET/OU/NON bit à bit. Il y a aussi deux instructions pour mettre un bit à 0 ou 1, ainsi que des branchements pour tester la valeur d'un bit dans un registre ou une adresse. Mais les instructions intéressantes sont :
* les instructions de copie mémoire-mémoire ;
* une instruction pour configurer un transfert DMA ;
* une instruction pour démarrer un transfert DMA à la prochaine instruction ;
* une instruction pour terminer l'exécution du programme I/O ;
* une instruction pour mettre la sortie d’interruption à 1.
Le 8089 contient des registres de 20/21 bits et des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses, alors que les registres de 16 bits sont des registres de données. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Le registre TP est le ''program counter'' associé à un canal DMA. Les deux canaux DMA exécutent chacun leur propre programme, ce qui fait qu'ils ont chacun leur propre ''program counter''. Mais pour le reste, les registres ne font pas la même chose selon qu'un transfert DMA est en cours ou non.
Lors d'un transfert DMA, les registres sont utilisés comme suit :
* Le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination.
* Le registre GC est facultatif : il pointe vers une table de 256 octets, utilisée pour faire de la traduction d'adresse.
* Le registre BC compte le nombre d'octets qu'il reste à transférer. Le registre MC mémorise un masque, qui est utilisé pour appliquer une opération de masquage avant d'écrire l'octet dans la destination.
* Le registre CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement par le 8089.
* Les autres registres ne sont pas utilisés.
En dehors d'un transfert DMA, les registres sont utilisés pour stocker des données, à l'exception de TP et de CC.
* Les trois registres d'adresse GA, GB et GC peuvent être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
* Le registre BC n'a pas de prédisposition en dehors des transferts DMA.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* MC est prédisposé à gérer un masque, qui est utilisé avec les instructions JMCE and JMCNE.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs de mémoire : un ''Channel Control Block'' et un ''Command Parameter Block''. Les deux sont placés en mémoire RAM, le 8089 y accède en lisant dans la mémoire RAM, et le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise. Je ne vais pas donner une explication complète de ces deux blocs, mais je vais quand même donner quelques détails.
Le ''Channel Control Block'' contient, pour chaque canal DMA, un octet de configuration. L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Elle est en réalité composée de deux adresses, parce que le 8089 et le 8086 utilisent la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
avzkestkv3dtdp4zw6pcx2la79qr4at
762992
762991
2026-04-05T17:09:25Z
Mewtow
31375
/* Les interconnexions entre coprocesseur IO, CPU et entrée-sorties */
762992
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir un dernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donneraient des routines d'interruptions déportées sur un autre processeur.
Avec un coprocesseur I/O, le CPU initie l'exécution d'un programme I/O sur le coprocesseur I/O. Le coprocesseur IO exécute alors le programme I/O dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
Les coprocesseurs I/O étaient très fréquents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Mais pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple, l'implémentation du DMA sur le coprocesseur I/O est aisée. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O sert d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecte le processeur au coprocesseur I/O. Mais utiliser un bus système a un gros avantage : le coprocesseur I/O peut gérer les transferts DMA !
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Pour initialiser le coprocesseur I/O, le processeur a juste besoin de l fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Le défaut à cela est que le CPU et le coprocesseur I/O se marchent sur les pieds pour accéder à la mémoire RAM.
Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à connecter cette RAM dédiée au coprocesseur I/O. Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire. Dans les deux cas, le programme I/O doit être copié dans cette mémoire séparée, ce qui n'est pas de la tarte.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeurs de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'Intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve les instructions classiques, avec quelques ajouts dédiés aux transferts DMA. Les calculs se limitent à l'addition, l'incrémentation, la décrémentation, et les ET/OU/NON bit à bit. Il y a aussi deux instructions pour mettre un bit à 0 ou 1, ainsi que des branchements pour tester la valeur d'un bit dans un registre ou une adresse. Mais les instructions intéressantes sont :
* les instructions de copie mémoire-mémoire ;
* une instruction pour configurer un transfert DMA ;
* une instruction pour démarrer un transfert DMA à la prochaine instruction ;
* une instruction pour terminer l'exécution du programme I/O ;
* une instruction pour mettre la sortie d’interruption à 1.
Le 8089 contient des registres de 20/21 bits et des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses, alors que les registres de 16 bits sont des registres de données. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Le registre TP est le ''program counter'' associé à un canal DMA. Les deux canaux DMA exécutent chacun leur propre programme, ce qui fait qu'ils ont chacun leur propre ''program counter''. Mais pour le reste, les registres ne font pas la même chose selon qu'un transfert DMA est en cours ou non.
Lors d'un transfert DMA, les registres sont utilisés comme suit :
* Le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination.
* Le registre GC est facultatif : il pointe vers une table de 256 octets, utilisée pour faire de la traduction d'adresse.
* Le registre BC compte le nombre d'octets qu'il reste à transférer. Le registre MC mémorise un masque, qui est utilisé pour appliquer une opération de masquage avant d'écrire l'octet dans la destination.
* Le registre CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement par le 8089.
* Les autres registres ne sont pas utilisés.
En dehors d'un transfert DMA, les registres sont utilisés pour stocker des données, à l'exception de TP et de CC.
* Les trois registres d'adresse GA, GB et GC peuvent être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
* Le registre BC n'a pas de prédisposition en dehors des transferts DMA.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* MC est prédisposé à gérer un masque, qui est utilisé avec les instructions JMCE and JMCNE.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs de mémoire : un ''Channel Control Block'' et un ''Command Parameter Block''. Les deux sont placés en mémoire RAM, le 8089 y accède en lisant dans la mémoire RAM, et le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise. Je ne vais pas donner une explication complète de ces deux blocs, mais je vais quand même donner quelques détails.
Le ''Channel Control Block'' contient, pour chaque canal DMA, un octet de configuration. L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Elle est en réalité composée de deux adresses, parce que le 8089 et le 8086 utilisent la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
0n71ie7n7mr1ldxspqplfpofcn9t2tl
762993
762992
2026-04-05T17:10:07Z
Mewtow
31375
/* Les interconnexions entre coprocesseur IO, CPU et entrée-sorties */
762993
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir un dernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donneraient des routines d'interruptions déportées sur un autre processeur.
Avec un coprocesseur I/O, le CPU initie l'exécution d'un programme I/O sur le coprocesseur I/O. Le coprocesseur IO exécute alors le programme I/O dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
Les coprocesseurs I/O étaient très fréquents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Mais pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de deux manières. Avec la première, il est connecté à un bus système, partagé avec le processeur. L'avantage est que le câblage du système est très simple, l'implémentation du DMA sur le coprocesseur I/O est aisée. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Avec la seconde méthode, le coprocesseur I/O sert d'intermédiaire entre les entrée-sorties et le processeur. Il fait en quelque sorte office de répartiteur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Pour initialiser le coprocesseur I/O, le processeur a juste besoin de l fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Le défaut à cela est que le CPU et le coprocesseur I/O se marchent sur les pieds pour accéder à la mémoire RAM.
Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à connecter cette RAM dédiée au coprocesseur I/O. Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire. Dans les deux cas, le programme I/O doit être copié dans cette mémoire séparée, ce qui n'est pas de la tarte.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecte le processeur au coprocesseur I/O. Mais utiliser un bus système a un gros avantage : le coprocesseur I/O peut gérer les transferts DMA !
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeurs de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'Intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve les instructions classiques, avec quelques ajouts dédiés aux transferts DMA. Les calculs se limitent à l'addition, l'incrémentation, la décrémentation, et les ET/OU/NON bit à bit. Il y a aussi deux instructions pour mettre un bit à 0 ou 1, ainsi que des branchements pour tester la valeur d'un bit dans un registre ou une adresse. Mais les instructions intéressantes sont :
* les instructions de copie mémoire-mémoire ;
* une instruction pour configurer un transfert DMA ;
* une instruction pour démarrer un transfert DMA à la prochaine instruction ;
* une instruction pour terminer l'exécution du programme I/O ;
* une instruction pour mettre la sortie d’interruption à 1.
Le 8089 contient des registres de 20/21 bits et des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses, alors que les registres de 16 bits sont des registres de données. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Le registre TP est le ''program counter'' associé à un canal DMA. Les deux canaux DMA exécutent chacun leur propre programme, ce qui fait qu'ils ont chacun leur propre ''program counter''. Mais pour le reste, les registres ne font pas la même chose selon qu'un transfert DMA est en cours ou non.
Lors d'un transfert DMA, les registres sont utilisés comme suit :
* Le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination.
* Le registre GC est facultatif : il pointe vers une table de 256 octets, utilisée pour faire de la traduction d'adresse.
* Le registre BC compte le nombre d'octets qu'il reste à transférer. Le registre MC mémorise un masque, qui est utilisé pour appliquer une opération de masquage avant d'écrire l'octet dans la destination.
* Le registre CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement par le 8089.
* Les autres registres ne sont pas utilisés.
En dehors d'un transfert DMA, les registres sont utilisés pour stocker des données, à l'exception de TP et de CC.
* Les trois registres d'adresse GA, GB et GC peuvent être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
* Le registre BC n'a pas de prédisposition en dehors des transferts DMA.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* MC est prédisposé à gérer un masque, qui est utilisé avec les instructions JMCE and JMCNE.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs de mémoire : un ''Channel Control Block'' et un ''Command Parameter Block''. Les deux sont placés en mémoire RAM, le 8089 y accède en lisant dans la mémoire RAM, et le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise. Je ne vais pas donner une explication complète de ces deux blocs, mais je vais quand même donner quelques détails.
Le ''Channel Control Block'' contient, pour chaque canal DMA, un octet de configuration. L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Elle est en réalité composée de deux adresses, parce que le 8089 et le 8086 utilisent la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
dut4oueutotlcu8ll6tia0fgusyg2ug
762994
762993
2026-04-05T17:47:25Z
Mewtow
31375
/* Les interconnexions entre coprocesseur IO, CPU et entrée-sorties */
762994
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir un dernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donneraient des routines d'interruptions déportées sur un autre processeur.
Avec un coprocesseur I/O, le CPU initie l'exécution d'un programme I/O sur le coprocesseur I/O. Le coprocesseur IO exécute alors le programme I/O dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
Les coprocesseurs I/O étaient très fréquents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Mais pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de plusieurs manières. L'essentiel est que le coprocesseur IO soit relié à la mémoire RAM, pour prendre en charge les transferts DMA. Rappelons en effet qu'un coprocesseur I/O est une sorte de contrôleur DMA sous stéroïde, ce qui requiert qu'il soit relié à la fois aux entrées-sorties et à la mémoire RAM. Il y a donc deux solutions : soit il est connecté à un bus système, soit il est utilisé en tant que répartiteur.
Utiliser un bus système a de nombreux avantages. Le câblage du système est très simple, l'implémentation du DMA sur le coprocesseur I/O est aisée. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Utiliser le coprocesseur I/O comme répartiteur fait que celui-ci sert d'intermédiaire entre les entrée-sorties et le reste de l'ordinateur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Pour initialiser le coprocesseur I/O, le processeur a juste besoin de l fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Le défaut à cela est que le CPU et le coprocesseur I/O se marchent sur les pieds pour accéder à la mémoire RAM.
Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à connecter cette RAM dédiée au coprocesseur I/O. Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire. Dans les deux cas, le programme I/O doit être copié dans cette mémoire séparée, ce qui n'est pas de la tarte.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus d'entrée-sortie séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus dédié aux IO.
Le bus processeur peut être un bus système ou un bus d'entrée-sortie dédiée, peu importe, tant qu'il connecte le processeur au coprocesseur I/O. Mais utiliser un bus système a un gros avantage : le coprocesseur I/O peut gérer les transferts DMA !
La connexion entre le coprocesseur I/O et les contrôleurs de périphérique peut s'implémenter de deux manières. La première utilise un bus qui relie le coprocesseur I/O et les contrôleurs de périphériques. L'implémentation demande peu de circuits et peu gérer un grand nombre de contrôleurs et de périphériques. En contrepartie que le coprocesseur ne peut communiquer qu'avec un seul contrôleur de périphérique à la fois. De tels coprocesseurs I/O sont appelés des '''''Selector Channel'''''.
Une autre possibilité connecte chaque contrôleur de périphérique sur un port dédié, avec une liaison point à point dédiée. Le cout en interconnexion est grand, cela ne marche pas au-delà de quelques périphériques, mais cela permet de gérer plusieurs communications simultanées. De tels coprocesseurs I/O sont appelés des '''''Multiplexer Channel'''''. Il est possible de les voir comme une sorte d'équivalent programmable du multiplexeur d'entrée-sortie des bus dédiés, mais pour les bus partagés.
[[File:Différence entre Selector channel et Multiplexor channel.png|centre|vignette|upright=2|Différence entre Selector channel et Multiplexor channel]]
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeurs de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'Intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve les instructions classiques, avec quelques ajouts dédiés aux transferts DMA. Les calculs se limitent à l'addition, l'incrémentation, la décrémentation, et les ET/OU/NON bit à bit. Il y a aussi deux instructions pour mettre un bit à 0 ou 1, ainsi que des branchements pour tester la valeur d'un bit dans un registre ou une adresse. Mais les instructions intéressantes sont :
* les instructions de copie mémoire-mémoire ;
* une instruction pour configurer un transfert DMA ;
* une instruction pour démarrer un transfert DMA à la prochaine instruction ;
* une instruction pour terminer l'exécution du programme I/O ;
* une instruction pour mettre la sortie d’interruption à 1.
Le 8089 contient des registres de 20/21 bits et des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses, alors que les registres de 16 bits sont des registres de données. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Le registre TP est le ''program counter'' associé à un canal DMA. Les deux canaux DMA exécutent chacun leur propre programme, ce qui fait qu'ils ont chacun leur propre ''program counter''. Mais pour le reste, les registres ne font pas la même chose selon qu'un transfert DMA est en cours ou non.
Lors d'un transfert DMA, les registres sont utilisés comme suit :
* Le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination.
* Le registre GC est facultatif : il pointe vers une table de 256 octets, utilisée pour faire de la traduction d'adresse.
* Le registre BC compte le nombre d'octets qu'il reste à transférer. Le registre MC mémorise un masque, qui est utilisé pour appliquer une opération de masquage avant d'écrire l'octet dans la destination.
* Le registre CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement par le 8089.
* Les autres registres ne sont pas utilisés.
En dehors d'un transfert DMA, les registres sont utilisés pour stocker des données, à l'exception de TP et de CC.
* Les trois registres d'adresse GA, GB et GC peuvent être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
* Le registre BC n'a pas de prédisposition en dehors des transferts DMA.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* MC est prédisposé à gérer un masque, qui est utilisé avec les instructions JMCE and JMCNE.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs de mémoire : un ''Channel Control Block'' et un ''Command Parameter Block''. Les deux sont placés en mémoire RAM, le 8089 y accède en lisant dans la mémoire RAM, et le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise. Je ne vais pas donner une explication complète de ces deux blocs, mais je vais quand même donner quelques détails.
Le ''Channel Control Block'' contient, pour chaque canal DMA, un octet de configuration. L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Elle est en réalité composée de deux adresses, parce que le 8089 et le 8086 utilisent la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
dtdf1ucz237wcsy1lp66g9r7ic1glcq
762995
762994
2026-04-05T17:52:55Z
Mewtow
31375
/* Les interconnexions entre coprocesseur IO, CPU et entrée-sorties */
762995
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir un dernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', que l'on peut voir comme des contrôleurs DMA améliorés, qui étaient rendus programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donneraient des routines d'interruptions déportées sur un autre processeur.
Avec un coprocesseur I/O, le CPU initie l'exécution d'un programme I/O sur le coprocesseur I/O. Le coprocesseur IO exécute alors le programme I/O dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
Les coprocesseurs I/O étaient très fréquents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Mais pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de plusieurs manières. L'essentiel est que le coprocesseur IO soit relié à la mémoire RAM, pour prendre en charge les transferts DMA. Rappelons en effet qu'un coprocesseur I/O est une sorte de contrôleur DMA sous stéroïde, ce qui requiert qu'il soit relié à la fois aux entrées-sorties et à la mémoire RAM. Il y a donc deux solutions : soit il est connecté à un bus système, soit il est utilisé en tant que répartiteur.
Utiliser un bus système a de nombreux avantages. Le câblage du système est très simple, l'implémentation du DMA sur le coprocesseur I/O est aisée. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Utiliser le coprocesseur I/O comme répartiteur fait que celui-ci sert d'intermédiaire entre les entrée-sorties et le reste de l'ordinateur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Pour initialiser le coprocesseur I/O, le processeur a juste besoin de l fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Le défaut à cela est que le CPU et le coprocesseur I/O se marchent sur les pieds pour accéder à la mémoire RAM.
Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à connecter cette RAM dédiée au coprocesseur I/O. Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire. Dans les deux cas, le programme I/O doit être copié dans cette mémoire séparée, ce qui n'est pas de la tarte.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus IO séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus IO.
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeurs de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'Intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve les instructions classiques, avec quelques ajouts dédiés aux transferts DMA. Les calculs se limitent à l'addition, l'incrémentation, la décrémentation, et les ET/OU/NON bit à bit. Il y a aussi deux instructions pour mettre un bit à 0 ou 1, ainsi que des branchements pour tester la valeur d'un bit dans un registre ou une adresse. Mais les instructions intéressantes sont :
* les instructions de copie mémoire-mémoire ;
* une instruction pour configurer un transfert DMA ;
* une instruction pour démarrer un transfert DMA à la prochaine instruction ;
* une instruction pour terminer l'exécution du programme I/O ;
* une instruction pour mettre la sortie d’interruption à 1.
Le 8089 contient des registres de 20/21 bits et des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses, alors que les registres de 16 bits sont des registres de données. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Le registre TP est le ''program counter'' associé à un canal DMA. Les deux canaux DMA exécutent chacun leur propre programme, ce qui fait qu'ils ont chacun leur propre ''program counter''. Mais pour le reste, les registres ne font pas la même chose selon qu'un transfert DMA est en cours ou non.
Lors d'un transfert DMA, les registres sont utilisés comme suit :
* Le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination.
* Le registre GC est facultatif : il pointe vers une table de 256 octets, utilisée pour faire de la traduction d'adresse.
* Le registre BC compte le nombre d'octets qu'il reste à transférer. Le registre MC mémorise un masque, qui est utilisé pour appliquer une opération de masquage avant d'écrire l'octet dans la destination.
* Le registre CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement par le 8089.
* Les autres registres ne sont pas utilisés.
En dehors d'un transfert DMA, les registres sont utilisés pour stocker des données, à l'exception de TP et de CC.
* Les trois registres d'adresse GA, GB et GC peuvent être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
* Le registre BC n'a pas de prédisposition en dehors des transferts DMA.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* MC est prédisposé à gérer un masque, qui est utilisé avec les instructions JMCE and JMCNE.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs de mémoire : un ''Channel Control Block'' et un ''Command Parameter Block''. Les deux sont placés en mémoire RAM, le 8089 y accède en lisant dans la mémoire RAM, et le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise. Je ne vais pas donner une explication complète de ces deux blocs, mais je vais quand même donner quelques détails.
Le ''Channel Control Block'' contient, pour chaque canal DMA, un octet de configuration. L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Elle est en réalité composée de deux adresses, parce que le 8089 et le 8086 utilisent la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
fb5kaal2vxvgy414xjcp89gymy7rl28
762996
762995
2026-04-05T17:54:22Z
Mewtow
31375
/* Les coprocesseurs d'entrée-sorties */
762996
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir un dernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', qui sont des contrôleurs DMA programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donneraient des routines d'interruptions déportées sur un autre processeur.
Avec un coprocesseur I/O, le CPU initie l'exécution d'un programme I/O sur le coprocesseur I/O. Le coprocesseur IO exécute alors le programme I/O dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
Les coprocesseurs I/O étaient très fréquents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Mais pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de plusieurs manières. L'essentiel est que le coprocesseur IO soit relié à la mémoire RAM, pour prendre en charge les transferts DMA. Rappelons en effet qu'un coprocesseur I/O est une sorte de contrôleur DMA sous stéroïde, ce qui requiert qu'il soit relié à la fois aux entrées-sorties et à la mémoire RAM. Il y a donc deux solutions : soit il est connecté à un bus système, soit il est utilisé en tant que répartiteur.
Utiliser un bus système a de nombreux avantages. Le câblage du système est très simple, l'implémentation du DMA sur le coprocesseur I/O est aisée. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Utiliser le coprocesseur I/O comme répartiteur fait que celui-ci sert d'intermédiaire entre les entrée-sorties et le reste de l'ordinateur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Pour initialiser le coprocesseur I/O, le processeur a juste besoin de l fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Le défaut à cela est que le CPU et le coprocesseur I/O se marchent sur les pieds pour accéder à la mémoire RAM.
Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à connecter cette RAM dédiée au coprocesseur I/O. Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire. Dans les deux cas, le programme I/O doit être copié dans cette mémoire séparée, ce qui n'est pas de la tarte.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus IO séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus IO.
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeurs de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'Intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve les instructions classiques, avec quelques ajouts dédiés aux transferts DMA. Les calculs se limitent à l'addition, l'incrémentation, la décrémentation, et les ET/OU/NON bit à bit. Il y a aussi deux instructions pour mettre un bit à 0 ou 1, ainsi que des branchements pour tester la valeur d'un bit dans un registre ou une adresse. Mais les instructions intéressantes sont :
* les instructions de copie mémoire-mémoire ;
* une instruction pour configurer un transfert DMA ;
* une instruction pour démarrer un transfert DMA à la prochaine instruction ;
* une instruction pour terminer l'exécution du programme I/O ;
* une instruction pour mettre la sortie d’interruption à 1.
Le 8089 contient des registres de 20/21 bits et des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses, alors que les registres de 16 bits sont des registres de données. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Le registre TP est le ''program counter'' associé à un canal DMA. Les deux canaux DMA exécutent chacun leur propre programme, ce qui fait qu'ils ont chacun leur propre ''program counter''. Mais pour le reste, les registres ne font pas la même chose selon qu'un transfert DMA est en cours ou non.
Lors d'un transfert DMA, les registres sont utilisés comme suit :
* Le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination.
* Le registre GC est facultatif : il pointe vers une table de 256 octets, utilisée pour faire de la traduction d'adresse.
* Le registre BC compte le nombre d'octets qu'il reste à transférer. Le registre MC mémorise un masque, qui est utilisé pour appliquer une opération de masquage avant d'écrire l'octet dans la destination.
* Le registre CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement par le 8089.
* Les autres registres ne sont pas utilisés.
En dehors d'un transfert DMA, les registres sont utilisés pour stocker des données, à l'exception de TP et de CC.
* Les trois registres d'adresse GA, GB et GC peuvent être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
* Le registre BC n'a pas de prédisposition en dehors des transferts DMA.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* MC est prédisposé à gérer un masque, qui est utilisé avec les instructions JMCE and JMCNE.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs de mémoire : un ''Channel Control Block'' et un ''Command Parameter Block''. Les deux sont placés en mémoire RAM, le 8089 y accède en lisant dans la mémoire RAM, et le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise. Je ne vais pas donner une explication complète de ces deux blocs, mais je vais quand même donner quelques détails.
Le ''Channel Control Block'' contient, pour chaque canal DMA, un octet de configuration. L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Elle est en réalité composée de deux adresses, parce que le 8089 et le 8086 utilisent la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
j5jrro6kgaxmxr9jndly439drk10n0l
762998
762996
2026-04-05T17:56:26Z
Mewtow
31375
/* Les coprocesseurs d'entrée-sorties */
762998
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir un dernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', qui sont des contrôleurs DMA programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donneraient des routines d'interruptions déportées sur un autre processeur.
Avec un coprocesseur I/O, le CPU initie l'exécution d'un programme I/O sur le coprocesseur I/O. Le coprocesseur IO exécute alors le programme I/O dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
Les coprocesseurs I/O étaient très fréquents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Mais pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Il était utilisé de concert avec un CPU Intel 8086.
Parfois, il arrive qu'un processeur normal soit réutilisé comme coprocesseur d'entrée-sortie. Un exemple assez récent est celui de la console de jeu Nintendo 3DS. Elle disposait d'un processeur principal de type ARM9, d'un coprocesseur pour les divisions qu'on abordera plus bas, et d'un second processeur ARM7. L'ARM 7 était utilisé comme coprocesseur d'I/O, ainsi que pour l'émulation de la console GBA.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de plusieurs manières. L'essentiel est que le coprocesseur IO soit relié à la mémoire RAM, pour prendre en charge les transferts DMA. Rappelons en effet qu'un coprocesseur I/O est une sorte de contrôleur DMA sous stéroïde, ce qui requiert qu'il soit relié à la fois aux entrées-sorties et à la mémoire RAM. Il y a donc deux solutions : soit il est connecté à un bus système, soit il est utilisé en tant que répartiteur.
Utiliser un bus système a de nombreux avantages. Le câblage du système est très simple, l'implémentation du DMA sur le coprocesseur I/O est aisée. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Utiliser le coprocesseur I/O comme répartiteur fait que celui-ci sert d'intermédiaire entre les entrée-sorties et le reste de l'ordinateur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Pour initialiser le coprocesseur I/O, le processeur a juste besoin de l fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Le défaut à cela est que le CPU et le coprocesseur I/O se marchent sur les pieds pour accéder à la mémoire RAM.
Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à connecter cette RAM dédiée au coprocesseur I/O. Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire. Dans les deux cas, le programme I/O doit être copié dans cette mémoire séparée, ce qui n'est pas de la tarte.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus IO séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus IO.
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeurs de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'Intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve les instructions classiques, avec quelques ajouts dédiés aux transferts DMA. Les calculs se limitent à l'addition, l'incrémentation, la décrémentation, et les ET/OU/NON bit à bit. Il y a aussi deux instructions pour mettre un bit à 0 ou 1, ainsi que des branchements pour tester la valeur d'un bit dans un registre ou une adresse. Mais les instructions intéressantes sont :
* les instructions de copie mémoire-mémoire ;
* une instruction pour configurer un transfert DMA ;
* une instruction pour démarrer un transfert DMA à la prochaine instruction ;
* une instruction pour terminer l'exécution du programme I/O ;
* une instruction pour mettre la sortie d’interruption à 1.
Le 8089 contient des registres de 20/21 bits et des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses, alors que les registres de 16 bits sont des registres de données. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Le registre TP est le ''program counter'' associé à un canal DMA. Les deux canaux DMA exécutent chacun leur propre programme, ce qui fait qu'ils ont chacun leur propre ''program counter''. Mais pour le reste, les registres ne font pas la même chose selon qu'un transfert DMA est en cours ou non.
Lors d'un transfert DMA, les registres sont utilisés comme suit :
* Le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination.
* Le registre GC est facultatif : il pointe vers une table de 256 octets, utilisée pour faire de la traduction d'adresse.
* Le registre BC compte le nombre d'octets qu'il reste à transférer. Le registre MC mémorise un masque, qui est utilisé pour appliquer une opération de masquage avant d'écrire l'octet dans la destination.
* Le registre CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement par le 8089.
* Les autres registres ne sont pas utilisés.
En dehors d'un transfert DMA, les registres sont utilisés pour stocker des données, à l'exception de TP et de CC.
* Les trois registres d'adresse GA, GB et GC peuvent être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
* Le registre BC n'a pas de prédisposition en dehors des transferts DMA.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* MC est prédisposé à gérer un masque, qui est utilisé avec les instructions JMCE and JMCNE.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs de mémoire : un ''Channel Control Block'' et un ''Command Parameter Block''. Les deux sont placés en mémoire RAM, le 8089 y accède en lisant dans la mémoire RAM, et le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise. Je ne vais pas donner une explication complète de ces deux blocs, mais je vais quand même donner quelques détails.
Le ''Channel Control Block'' contient, pour chaque canal DMA, un octet de configuration. L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Elle est en réalité composée de deux adresses, parce que le 8089 et le 8086 utilisent la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
owzdtrus285k1f6polev2y10bt45v66
762999
762998
2026-04-05T17:58:13Z
Mewtow
31375
/* Les coprocesseurs d'entrée-sorties */
762999
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir un dernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', qui sont des contrôleurs DMA programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donneraient des routines d'interruptions déportées sur un autre processeur.
Avec un coprocesseur I/O, le CPU initie l'exécution d'un programme I/O sur le coprocesseur I/O. Le coprocesseur IO exécute alors le programme I/O dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
Les coprocesseurs I/O étaient très fréquents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Mais pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Parfois, il est arrivé qu'un processeur normal soit utilisé comme coprocesseur d'entrée-sortie. Un exemple assez récent est celui de la console de jeu Nintendo 3DS. Elle disposait d'un processeur principal de type ARM9, d'un coprocesseur pour les divisions qu'on abordera plus bas, et d'un second processeur ARM7. L'ARM 7 était utilisé comme coprocesseur d'I/O, ainsi que pour l'émulation de la console GBA.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de plusieurs manières. L'essentiel est que le coprocesseur IO soit relié à la mémoire RAM, pour prendre en charge les transferts DMA. Rappelons en effet qu'un coprocesseur I/O est une sorte de contrôleur DMA sous stéroïde, ce qui requiert qu'il soit relié à la fois aux entrées-sorties et à la mémoire RAM. Il y a donc deux solutions : soit il est connecté à un bus système, soit il est utilisé en tant que répartiteur.
Utiliser un bus système a de nombreux avantages. Le câblage du système est très simple, l'implémentation du DMA sur le coprocesseur I/O est aisée. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Utiliser le coprocesseur I/O comme répartiteur fait que celui-ci sert d'intermédiaire entre les entrée-sorties et le reste de l'ordinateur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Pour initialiser le coprocesseur I/O, le processeur a juste besoin de l fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Le défaut à cela est que le CPU et le coprocesseur I/O se marchent sur les pieds pour accéder à la mémoire RAM.
Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à connecter cette RAM dédiée au coprocesseur I/O. Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire. Dans les deux cas, le programme I/O doit être copié dans cette mémoire séparée, ce qui n'est pas de la tarte.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus IO séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus IO.
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeurs de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'Intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve les instructions classiques, avec quelques ajouts dédiés aux transferts DMA. Les calculs se limitent à l'addition, l'incrémentation, la décrémentation, et les ET/OU/NON bit à bit. Il y a aussi deux instructions pour mettre un bit à 0 ou 1, ainsi que des branchements pour tester la valeur d'un bit dans un registre ou une adresse. Mais les instructions intéressantes sont :
* les instructions de copie mémoire-mémoire ;
* une instruction pour configurer un transfert DMA ;
* une instruction pour démarrer un transfert DMA à la prochaine instruction ;
* une instruction pour terminer l'exécution du programme I/O ;
* une instruction pour mettre la sortie d’interruption à 1.
Le 8089 contient des registres de 20/21 bits et des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses, alors que les registres de 16 bits sont des registres de données. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Le registre TP est le ''program counter'' associé à un canal DMA. Les deux canaux DMA exécutent chacun leur propre programme, ce qui fait qu'ils ont chacun leur propre ''program counter''. Mais pour le reste, les registres ne font pas la même chose selon qu'un transfert DMA est en cours ou non.
Lors d'un transfert DMA, les registres sont utilisés comme suit :
* Le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination.
* Le registre GC est facultatif : il pointe vers une table de 256 octets, utilisée pour faire de la traduction d'adresse.
* Le registre BC compte le nombre d'octets qu'il reste à transférer. Le registre MC mémorise un masque, qui est utilisé pour appliquer une opération de masquage avant d'écrire l'octet dans la destination.
* Le registre CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement par le 8089.
* Les autres registres ne sont pas utilisés.
En dehors d'un transfert DMA, les registres sont utilisés pour stocker des données, à l'exception de TP et de CC.
* Les trois registres d'adresse GA, GB et GC peuvent être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
* Le registre BC n'a pas de prédisposition en dehors des transferts DMA.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* MC est prédisposé à gérer un masque, qui est utilisé avec les instructions JMCE and JMCNE.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs de mémoire : un ''Channel Control Block'' et un ''Command Parameter Block''. Les deux sont placés en mémoire RAM, le 8089 y accède en lisant dans la mémoire RAM, et le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise. Je ne vais pas donner une explication complète de ces deux blocs, mais je vais quand même donner quelques détails.
Le ''Channel Control Block'' contient, pour chaque canal DMA, un octet de configuration. L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Elle est en réalité composée de deux adresses, parce que le 8089 et le 8086 utilisent la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
1pwd1xg9038uhx2x61rf92tzhgllq3j
763000
762999
2026-04-05T17:58:27Z
Mewtow
31375
/* Les coprocesseurs d'entrée-sorties */
763000
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir un dernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', qui sont des contrôleurs DMA programmables. Ils déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O étaient très fréquents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Mais pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. Parfois, il est arrivé qu'un processeur normal soit utilisé comme coprocesseur d'entrée-sortie. Un exemple assez récent est celui de la console de jeu Nintendo 3DS. Elle disposait d'un processeur principal de type ARM9, d'un coprocesseur pour les divisions qu'on abordera plus bas, et d'un second processeur ARM7. L'ARM 7 était utilisé comme coprocesseur d'I/O, ainsi que pour l'émulation de la console GBA.
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donneraient des routines d'interruptions déportées sur un autre processeur.
Avec un coprocesseur I/O, le CPU initie l'exécution d'un programme I/O sur le coprocesseur I/O. Le coprocesseur IO exécute alors le programme I/O dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de plusieurs manières. L'essentiel est que le coprocesseur IO soit relié à la mémoire RAM, pour prendre en charge les transferts DMA. Rappelons en effet qu'un coprocesseur I/O est une sorte de contrôleur DMA sous stéroïde, ce qui requiert qu'il soit relié à la fois aux entrées-sorties et à la mémoire RAM. Il y a donc deux solutions : soit il est connecté à un bus système, soit il est utilisé en tant que répartiteur.
Utiliser un bus système a de nombreux avantages. Le câblage du système est très simple, l'implémentation du DMA sur le coprocesseur I/O est aisée. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Utiliser le coprocesseur I/O comme répartiteur fait que celui-ci sert d'intermédiaire entre les entrée-sorties et le reste de l'ordinateur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Pour initialiser le coprocesseur I/O, le processeur a juste besoin de l fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Le défaut à cela est que le CPU et le coprocesseur I/O se marchent sur les pieds pour accéder à la mémoire RAM.
Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à connecter cette RAM dédiée au coprocesseur I/O. Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire. Dans les deux cas, le programme I/O doit être copié dans cette mémoire séparée, ce qui n'est pas de la tarte.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus IO séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus IO.
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeurs de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'Intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve les instructions classiques, avec quelques ajouts dédiés aux transferts DMA. Les calculs se limitent à l'addition, l'incrémentation, la décrémentation, et les ET/OU/NON bit à bit. Il y a aussi deux instructions pour mettre un bit à 0 ou 1, ainsi que des branchements pour tester la valeur d'un bit dans un registre ou une adresse. Mais les instructions intéressantes sont :
* les instructions de copie mémoire-mémoire ;
* une instruction pour configurer un transfert DMA ;
* une instruction pour démarrer un transfert DMA à la prochaine instruction ;
* une instruction pour terminer l'exécution du programme I/O ;
* une instruction pour mettre la sortie d’interruption à 1.
Le 8089 contient des registres de 20/21 bits et des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses, alors que les registres de 16 bits sont des registres de données. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Le registre TP est le ''program counter'' associé à un canal DMA. Les deux canaux DMA exécutent chacun leur propre programme, ce qui fait qu'ils ont chacun leur propre ''program counter''. Mais pour le reste, les registres ne font pas la même chose selon qu'un transfert DMA est en cours ou non.
Lors d'un transfert DMA, les registres sont utilisés comme suit :
* Le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination.
* Le registre GC est facultatif : il pointe vers une table de 256 octets, utilisée pour faire de la traduction d'adresse.
* Le registre BC compte le nombre d'octets qu'il reste à transférer. Le registre MC mémorise un masque, qui est utilisé pour appliquer une opération de masquage avant d'écrire l'octet dans la destination.
* Le registre CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement par le 8089.
* Les autres registres ne sont pas utilisés.
En dehors d'un transfert DMA, les registres sont utilisés pour stocker des données, à l'exception de TP et de CC.
* Les trois registres d'adresse GA, GB et GC peuvent être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
* Le registre BC n'a pas de prédisposition en dehors des transferts DMA.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* MC est prédisposé à gérer un masque, qui est utilisé avec les instructions JMCE and JMCNE.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs de mémoire : un ''Channel Control Block'' et un ''Command Parameter Block''. Les deux sont placés en mémoire RAM, le 8089 y accède en lisant dans la mémoire RAM, et le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise. Je ne vais pas donner une explication complète de ces deux blocs, mais je vais quand même donner quelques détails.
Le ''Channel Control Block'' contient, pour chaque canal DMA, un octet de configuration. L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Elle est en réalité composée de deux adresses, parce que le 8089 et le 8086 utilisent la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
o4610j6rsjlrcw2v1aysilahlgh39fx
763001
763000
2026-04-05T18:08:04Z
Mewtow
31375
/* Les coprocesseurs d'entrée-sorties */
763001
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir un dernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', qui déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les coprocesseurs I/O étaient très fréquents sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. Mais pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel, l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. les consoles de jeu utilisent souvent des coprocesseurs I/O. Pour être précis, elles utilisent un processeur normal comme coprocesseur d'entrée-sortie. Un exemple est celui de la console de jeu Nintendo 3DS, qui disposait d'un CPU ARM9, d'un coprocesseur pour les divisions qu'on abordera plus bas, et d'un processeur ARM7. L'ARM 7 était utilisé comme coprocesseur d'I/O, ainsi que pour l'émulation de la console GBA.
Pour simplifier, les coprocesseurs I/O sont des '''contrôleurs DMA programmables'''.
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donneraient des routines d'interruptions déportées sur un autre processeur.
Avec un coprocesseur I/O, le CPU initie l'exécution d'un programme I/O sur le coprocesseur I/O. Le coprocesseur IO exécute alors le programme I/O dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de plusieurs manières. L'essentiel est que le coprocesseur IO soit relié à la mémoire RAM, pour prendre en charge les transferts DMA. Rappelons en effet qu'un coprocesseur I/O est une sorte de contrôleur DMA sous stéroïde, ce qui requiert qu'il soit relié à la fois aux entrées-sorties et à la mémoire RAM. Il y a donc deux solutions : soit il est connecté à un bus système, soit il est utilisé en tant que répartiteur.
Utiliser un bus système a de nombreux avantages. Le câblage du système est très simple, l'implémentation du DMA sur le coprocesseur I/O est aisée. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Utiliser le coprocesseur I/O comme répartiteur fait que celui-ci sert d'intermédiaire entre les entrée-sorties et le reste de l'ordinateur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Pour initialiser le coprocesseur I/O, le processeur a juste besoin de l fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Le défaut à cela est que le CPU et le coprocesseur I/O se marchent sur les pieds pour accéder à la mémoire RAM.
Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à connecter cette RAM dédiée au coprocesseur I/O. Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire. Dans les deux cas, le programme I/O doit être copié dans cette mémoire séparée, ce qui n'est pas de la tarte.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus IO séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus IO.
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeurs de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'Intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve les instructions classiques, avec quelques ajouts dédiés aux transferts DMA. Les calculs se limitent à l'addition, l'incrémentation, la décrémentation, et les ET/OU/NON bit à bit. Il y a aussi deux instructions pour mettre un bit à 0 ou 1, ainsi que des branchements pour tester la valeur d'un bit dans un registre ou une adresse. Mais les instructions intéressantes sont :
* les instructions de copie mémoire-mémoire ;
* une instruction pour configurer un transfert DMA ;
* une instruction pour démarrer un transfert DMA à la prochaine instruction ;
* une instruction pour terminer l'exécution du programme I/O ;
* une instruction pour mettre la sortie d’interruption à 1.
Le 8089 contient des registres de 20/21 bits et des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses, alors que les registres de 16 bits sont des registres de données. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Le registre TP est le ''program counter'' associé à un canal DMA. Les deux canaux DMA exécutent chacun leur propre programme, ce qui fait qu'ils ont chacun leur propre ''program counter''. Mais pour le reste, les registres ne font pas la même chose selon qu'un transfert DMA est en cours ou non.
Lors d'un transfert DMA, les registres sont utilisés comme suit :
* Le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination.
* Le registre GC est facultatif : il pointe vers une table de 256 octets, utilisée pour faire de la traduction d'adresse.
* Le registre BC compte le nombre d'octets qu'il reste à transférer. Le registre MC mémorise un masque, qui est utilisé pour appliquer une opération de masquage avant d'écrire l'octet dans la destination.
* Le registre CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement par le 8089.
* Les autres registres ne sont pas utilisés.
En dehors d'un transfert DMA, les registres sont utilisés pour stocker des données, à l'exception de TP et de CC.
* Les trois registres d'adresse GA, GB et GC peuvent être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
* Le registre BC n'a pas de prédisposition en dehors des transferts DMA.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* MC est prédisposé à gérer un masque, qui est utilisé avec les instructions JMCE and JMCNE.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs de mémoire : un ''Channel Control Block'' et un ''Command Parameter Block''. Les deux sont placés en mémoire RAM, le 8089 y accède en lisant dans la mémoire RAM, et le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise. Je ne vais pas donner une explication complète de ces deux blocs, mais je vais quand même donner quelques détails.
Le ''Channel Control Block'' contient, pour chaque canal DMA, un octet de configuration. L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Elle est en réalité composée de deux adresses, parce que le 8089 et le 8086 utilisent la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
is1lmb512imkknc8qvkow9caobhdnwj
763002
763001
2026-04-05T18:12:06Z
Mewtow
31375
/* Les coprocesseurs d'entrée-sorties */
763002
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir un dernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', qui déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Pour simplifier, les coprocesseurs I/O sont des '''contrôleurs DMA programmables'''. Ils sont apparus sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. A l'époque, communiquer avec des périphérique demandait de faire des transferts DMA, mais les contrôleurs DMA n'existaient pas. Alors les contrôleurs DMA étaient émulés avec des processeurs dédiés, qui s'occupaient des transferts DMA, au jeu d'instruction spécialisé pour gérer des entrées-sorties. Et vu que c'était des processeurs, ils étaient programmables, dans une certaine mesure. C'était l'époque des ''channel I/O'' des vieux ''mainframes''.
Pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel : l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. les consoles de jeu utilisent souvent des coprocesseurs I/O. Pour être précis, elles utilisent un processeur normal comme coprocesseur d'entrée-sortie. Un exemple est celui de la console de jeu Nintendo 3DS, qui disposait d'un CPU ARM9, d'un coprocesseur pour les divisions qu'on abordera plus bas, et d'un processeur ARM7. L'ARM 7 était utilisé comme coprocesseur d'I/O, ainsi que pour l'émulation de la console GBA.
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donneraient des routines d'interruptions déportées sur un autre processeur.
Avec un coprocesseur I/O, le CPU initie l'exécution d'un programme I/O sur le coprocesseur I/O. Le coprocesseur IO exécute alors le programme I/O dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de plusieurs manières. L'essentiel est que le coprocesseur IO soit relié à la mémoire RAM, pour prendre en charge les transferts DMA. Rappelons en effet qu'un coprocesseur I/O est une sorte de contrôleur DMA sous stéroïde, ce qui requiert qu'il soit relié à la fois aux entrées-sorties et à la mémoire RAM. Il y a donc deux solutions : soit il est connecté à un bus système, soit il est utilisé en tant que répartiteur.
Utiliser un bus système a de nombreux avantages. Le câblage du système est très simple, l'implémentation du DMA sur le coprocesseur I/O est aisée. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Utiliser le coprocesseur I/O comme répartiteur fait que celui-ci sert d'intermédiaire entre les entrée-sorties et le reste de l'ordinateur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Pour initialiser le coprocesseur I/O, le processeur a juste besoin de l fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Le défaut à cela est que le CPU et le coprocesseur I/O se marchent sur les pieds pour accéder à la mémoire RAM.
Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à connecter cette RAM dédiée au coprocesseur I/O. Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire. Dans les deux cas, le programme I/O doit être copié dans cette mémoire séparée, ce qui n'est pas de la tarte.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus IO séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus IO.
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeurs de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'Intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve les instructions classiques, avec quelques ajouts dédiés aux transferts DMA. Les calculs se limitent à l'addition, l'incrémentation, la décrémentation, et les ET/OU/NON bit à bit. Il y a aussi deux instructions pour mettre un bit à 0 ou 1, ainsi que des branchements pour tester la valeur d'un bit dans un registre ou une adresse. Mais les instructions intéressantes sont :
* les instructions de copie mémoire-mémoire ;
* une instruction pour configurer un transfert DMA ;
* une instruction pour démarrer un transfert DMA à la prochaine instruction ;
* une instruction pour terminer l'exécution du programme I/O ;
* une instruction pour mettre la sortie d’interruption à 1.
Le 8089 contient des registres de 20/21 bits et des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses, alors que les registres de 16 bits sont des registres de données. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Le registre TP est le ''program counter'' associé à un canal DMA. Les deux canaux DMA exécutent chacun leur propre programme, ce qui fait qu'ils ont chacun leur propre ''program counter''. Mais pour le reste, les registres ne font pas la même chose selon qu'un transfert DMA est en cours ou non.
Lors d'un transfert DMA, les registres sont utilisés comme suit :
* Le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination.
* Le registre GC est facultatif : il pointe vers une table de 256 octets, utilisée pour faire de la traduction d'adresse.
* Le registre BC compte le nombre d'octets qu'il reste à transférer. Le registre MC mémorise un masque, qui est utilisé pour appliquer une opération de masquage avant d'écrire l'octet dans la destination.
* Le registre CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement par le 8089.
* Les autres registres ne sont pas utilisés.
En dehors d'un transfert DMA, les registres sont utilisés pour stocker des données, à l'exception de TP et de CC.
* Les trois registres d'adresse GA, GB et GC peuvent être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
* Le registre BC n'a pas de prédisposition en dehors des transferts DMA.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* MC est prédisposé à gérer un masque, qui est utilisé avec les instructions JMCE and JMCNE.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs de mémoire : un ''Channel Control Block'' et un ''Command Parameter Block''. Les deux sont placés en mémoire RAM, le 8089 y accède en lisant dans la mémoire RAM, et le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise. Je ne vais pas donner une explication complète de ces deux blocs, mais je vais quand même donner quelques détails.
Le ''Channel Control Block'' contient, pour chaque canal DMA, un octet de configuration. L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Elle est en réalité composée de deux adresses, parce que le 8089 et le 8086 utilisent la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
7vlhvlsv7sw457r17zg0k8qiwr8b3on
763003
763002
2026-04-05T18:26:28Z
Mewtow
31375
/* Les coprocesseurs d'entrée-sorties */
763003
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir un dernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', qui déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Pour simplifier, les coprocesseurs I/O sont des '''contrôleurs DMA programmables'''. Les plus simples sont simplement des contrôleurs DMA émulés avec un processeur, mais la majorité est capables d'enchainer plusieurs transferts DMA à la suite l'un de l'autre.
Ils sont apparus sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. A l'époque, communiquer avec des périphérique demandait de faire des transferts DMA, mais les contrôleurs DMA n'existaient pas. Alors les contrôleurs DMA étaient émulés avec des processeurs dédiés, qui s'occupaient des transferts DMA, au jeu d'instruction spécialisé pour gérer des entrées-sorties. Et vu que c'était des processeurs, ils étaient programmables, dans une certaine mesure. C'était l'époque des ''channel I/O'' des vieux ''mainframes''.
Pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel : l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. les consoles de jeu utilisent souvent des coprocesseurs I/O. Pour être précis, elles utilisent un processeur normal comme coprocesseur d'entrée-sortie. Un exemple est celui de la console de jeu Nintendo 3DS, qui disposait d'un CPU ARM9, d'un coprocesseur pour les divisions qu'on abordera plus bas, et d'un processeur ARM7. L'ARM 7 était utilisé comme coprocesseur d'I/O, ainsi que pour l'émulation de la console GBA.
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donneraient des routines d'interruptions déportées sur un autre processeur.
Avec un coprocesseur I/O, le CPU initie l'exécution d'un programme I/O sur le coprocesseur I/O. Le coprocesseur IO exécute alors le programme I/O dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de plusieurs manières. L'essentiel est que le coprocesseur IO soit relié à la mémoire RAM, pour prendre en charge les transferts DMA. Rappelons en effet qu'un coprocesseur I/O est une sorte de contrôleur DMA sous stéroïde, ce qui requiert qu'il soit relié à la fois aux entrées-sorties et à la mémoire RAM. Il y a donc deux solutions : soit il est connecté à un bus système, soit il est utilisé en tant que répartiteur.
Utiliser un bus système a de nombreux avantages. Le câblage du système est très simple, l'implémentation du DMA sur le coprocesseur I/O est aisée. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Utiliser le coprocesseur I/O comme répartiteur fait que celui-ci sert d'intermédiaire entre les entrée-sorties et le reste de l'ordinateur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Pour initialiser le coprocesseur I/O, le processeur a juste besoin de l fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Le défaut à cela est que le CPU et le coprocesseur I/O se marchent sur les pieds pour accéder à la mémoire RAM.
Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à connecter cette RAM dédiée au coprocesseur I/O. Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire. Dans les deux cas, le programme I/O doit être copié dans cette mémoire séparée, ce qui n'est pas de la tarte.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus IO séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus IO.
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeurs de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'Intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve les instructions classiques, avec quelques ajouts dédiés aux transferts DMA. Les calculs se limitent à l'addition, l'incrémentation, la décrémentation, et les ET/OU/NON bit à bit. Il y a aussi deux instructions pour mettre un bit à 0 ou 1, ainsi que des branchements pour tester la valeur d'un bit dans un registre ou une adresse. Mais les instructions intéressantes sont :
* les instructions de copie mémoire-mémoire ;
* une instruction pour configurer un transfert DMA ;
* une instruction pour démarrer un transfert DMA à la prochaine instruction ;
* une instruction pour terminer l'exécution du programme I/O ;
* une instruction pour mettre la sortie d’interruption à 1.
Le 8089 contient des registres de 20/21 bits et des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses, alors que les registres de 16 bits sont des registres de données. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Le registre TP est le ''program counter'' associé à un canal DMA. Les deux canaux DMA exécutent chacun leur propre programme, ce qui fait qu'ils ont chacun leur propre ''program counter''. Mais pour le reste, les registres ne font pas la même chose selon qu'un transfert DMA est en cours ou non.
Lors d'un transfert DMA, les registres sont utilisés comme suit :
* Le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination.
* Le registre GC est facultatif : il pointe vers une table de 256 octets, utilisée pour faire de la traduction d'adresse.
* Le registre BC compte le nombre d'octets qu'il reste à transférer. Le registre MC mémorise un masque, qui est utilisé pour appliquer une opération de masquage avant d'écrire l'octet dans la destination.
* Le registre CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement par le 8089.
* Les autres registres ne sont pas utilisés.
En dehors d'un transfert DMA, les registres sont utilisés pour stocker des données, à l'exception de TP et de CC.
* Les trois registres d'adresse GA, GB et GC peuvent être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
* Le registre BC n'a pas de prédisposition en dehors des transferts DMA.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* MC est prédisposé à gérer un masque, qui est utilisé avec les instructions JMCE and JMCNE.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs de mémoire : un ''Channel Control Block'' et un ''Command Parameter Block''. Les deux sont placés en mémoire RAM, le 8089 y accède en lisant dans la mémoire RAM, et le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise. Je ne vais pas donner une explication complète de ces deux blocs, mais je vais quand même donner quelques détails.
Le ''Channel Control Block'' contient, pour chaque canal DMA, un octet de configuration. L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Elle est en réalité composée de deux adresses, parce que le 8089 et le 8086 utilisent la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
0jp0dkhludczasd2y5l411t8ihxs83g
763004
763003
2026-04-05T18:29:21Z
Mewtow
31375
/* Les coprocesseurs d'entrée-sorties */
763004
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir un dernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', qui déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Pour simplifier, les coprocesseurs I/O sont des '''contrôleurs DMA programmables'''. Les plus simples sont simplement des contrôleurs DMA émulés avec un processeur, mais la majorité est capables d'enchainer plusieurs transferts DMA à la suite l'un de l'autre. Et certains d'entre eux savaient se débrouiller avec la mémoire virtuelle, avec des capacités de traduction d'adresse virtuelles en adresses physiques.
Ils sont apparus sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. A l'époque, communiquer avec des périphérique demandait de faire des transferts DMA, mais les contrôleurs DMA n'existaient pas. Alors les contrôleurs DMA étaient émulés avec des processeurs dédiés, qui s'occupaient des transferts DMA, au jeu d'instruction spécialisé pour gérer des entrées-sorties. Et vu que c'était des processeurs, ils étaient programmables, dans une certaine mesure. C'était l'époque des ''channel I/O'' des vieux ''mainframes''.
Pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel : l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. les consoles de jeu utilisent souvent des coprocesseurs I/O. Pour être précis, elles utilisent un processeur normal comme coprocesseur d'entrée-sortie. Un exemple est celui de la console de jeu Nintendo 3DS, qui disposait d'un CPU ARM9, d'un coprocesseur pour les divisions qu'on abordera plus bas, et d'un processeur ARM7. L'ARM 7 était utilisé comme coprocesseur d'I/O, ainsi que pour l'émulation de la console GBA.
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Il y a généralement un programme I/O pour chaque périphérique. Par exemple, il y a un programme pour le contrôleur de disquette, un autre pour le contrôleur du lecteur de bande magnétique, un autre pour la carte graphique, etc. Les programmes I/O sont fournis par le système d'exploitation et on peut les voir comme des pilotes de périphériques simplifiés. Ils sont équivalents à ce que donneraient des routines d'interruptions déportées sur un autre processeur.
Avec un coprocesseur I/O, le CPU initie l'exécution d'un programme I/O sur le coprocesseur I/O. Le coprocesseur IO exécute alors le programme I/O dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de plusieurs manières. L'essentiel est que le coprocesseur IO soit relié à la mémoire RAM, pour prendre en charge les transferts DMA. Rappelons en effet qu'un coprocesseur I/O est une sorte de contrôleur DMA sous stéroïde, ce qui requiert qu'il soit relié à la fois aux entrées-sorties et à la mémoire RAM. Il y a donc deux solutions : soit il est connecté à un bus système, soit il est utilisé en tant que répartiteur.
Utiliser un bus système a de nombreux avantages. Le câblage du système est très simple, l'implémentation du DMA sur le coprocesseur I/O est aisée. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Utiliser le coprocesseur I/O comme répartiteur fait que celui-ci sert d'intermédiaire entre les entrée-sorties et le reste de l'ordinateur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Pour initialiser le coprocesseur I/O, le processeur a juste besoin de l fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Le défaut à cela est que le CPU et le coprocesseur I/O se marchent sur les pieds pour accéder à la mémoire RAM.
Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à connecter cette RAM dédiée au coprocesseur I/O. Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire. Dans les deux cas, le programme I/O doit être copié dans cette mémoire séparée, ce qui n'est pas de la tarte.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus IO séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus IO.
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeurs de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'Intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve les instructions classiques, avec quelques ajouts dédiés aux transferts DMA. Les calculs se limitent à l'addition, l'incrémentation, la décrémentation, et les ET/OU/NON bit à bit. Il y a aussi deux instructions pour mettre un bit à 0 ou 1, ainsi que des branchements pour tester la valeur d'un bit dans un registre ou une adresse. Mais les instructions intéressantes sont :
* les instructions de copie mémoire-mémoire ;
* une instruction pour configurer un transfert DMA ;
* une instruction pour démarrer un transfert DMA à la prochaine instruction ;
* une instruction pour terminer l'exécution du programme I/O ;
* une instruction pour mettre la sortie d’interruption à 1.
Le 8089 contient des registres de 20/21 bits et des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses, alors que les registres de 16 bits sont des registres de données. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Le registre TP est le ''program counter'' associé à un canal DMA. Les deux canaux DMA exécutent chacun leur propre programme, ce qui fait qu'ils ont chacun leur propre ''program counter''. Mais pour le reste, les registres ne font pas la même chose selon qu'un transfert DMA est en cours ou non.
Lors d'un transfert DMA, les registres sont utilisés comme suit :
* Le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination.
* Le registre GC est facultatif : il pointe vers une table de 256 octets, utilisée pour faire de la traduction d'adresse.
* Le registre BC compte le nombre d'octets qu'il reste à transférer. Le registre MC mémorise un masque, qui est utilisé pour appliquer une opération de masquage avant d'écrire l'octet dans la destination.
* Le registre CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement par le 8089.
* Les autres registres ne sont pas utilisés.
En dehors d'un transfert DMA, les registres sont utilisés pour stocker des données, à l'exception de TP et de CC.
* Les trois registres d'adresse GA, GB et GC peuvent être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
* Le registre BC n'a pas de prédisposition en dehors des transferts DMA.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* MC est prédisposé à gérer un masque, qui est utilisé avec les instructions JMCE and JMCNE.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs de mémoire : un ''Channel Control Block'' et un ''Command Parameter Block''. Les deux sont placés en mémoire RAM, le 8089 y accède en lisant dans la mémoire RAM, et le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise. Je ne vais pas donner une explication complète de ces deux blocs, mais je vais quand même donner quelques détails.
Le ''Channel Control Block'' contient, pour chaque canal DMA, un octet de configuration. L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Elle est en réalité composée de deux adresses, parce que le 8089 et le 8086 utilisent la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
nvnnupv5lel8haba25pvt9id1b5s4lg
763005
763004
2026-04-05T18:30:43Z
Mewtow
31375
/* Les coprocesseurs d'entrée-sorties */
763005
wikitext
text/x-wiki
Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et reste connecté au bus tant que le périphérique n'a pas traité sa demande, que ce soit une lecture, ou une écriture. Mais les périphériques sont tellement lents que le processeur passe son temps à attendre le périphérique. Aussi, il a fallu trouver une solution pour simplifier la communication avec les périphériques.
==Le contrôleur de périphériques==
Pour résoudre ce problème, il suffit d'intercaler un intermédiaire entre le périphérique et le reste de l'ordinateur. Cet intermédiaire s'appelle le '''contrôleur de périphériques'''. Les contrôleurs de périphérique vont du simple circuit de quelques centaines de transistors à un microcontrôleur très puissant. Le contrôleur de périphérique est généralement placé sur la carte mère, mais il peut être intégré directement dans le périphérique, tout dépend de la situation.
Le processeur envoie au contrôleur de périphérique des « commandes », des valeurs numériques auxquelles le périphérique répond en effectuant un ensemble d'actions préprogrammées. Le contrôleur de périphérique reçoit les commandes envoyées par le processeur et pilote le périphérique de façon à faire ce qui est demandé. Le boulot du contrôleur de périphérique est de générer des signaux de commande qui déclencheront une action effectuée par le périphérique. L'analogie avec le séquenceur d'un processeur est possible.
[[File:Controleur de périphériques.png|centre|vignette|upright=2.5|Contrôleur de périphériques]]
===Les registres d’interfaçage===
Pour faire son travail, le contrôleur de périphérique doit avoir de quoi mémoriser les données à échanger entre processeur et périphérique. Pour cela, il contient des '''registres d'interfaçage''' entre le processeur et les entrées-sorties. Pour simplifier, les registres d’interfaçage sont de trois types : les registres de données, les registres de commande et les registres d'état.
* Les ''registres de données'' permettent l'échange de données entre le processeur et les périphériques. On trouve généralement un registre de lecture et un registre d'écriture, mais il se peut que les deux soient fusionnés en un seul registre d’interfaçage de données.
* Les ''registres de commande'' sont des registres qui mémorisent les commandes envoyées par le processeur. Quand le processeur veut envoyer une commande au périphérique, il écrit la commande en question dans ce ou ces registres.
* Enfin, beaucoup de périphériques ont un ''registre d'état'', lisible par le processeur, qui contient des informations sur l'état du périphérique. Ils servent notamment à indiquer au processeur que le périphérique est disponible, qu'il est en train d’exécuter une commande, qu'il est utilisé par un autre processeur, etc. Ils peuvent parfois signaler des erreurs de configuration ou des pannes touchant un périphérique.
[[File:Registres d'interfaçage.png|centre|vignette|upright=2|Registres d'interfaçage.]]
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé : le registre maintient les informations à transmettre tant que le périphérique en a besoin. Une écriture ou en envoi de commande simple demande donc au processeur d'écrire dans les registres d’interfaçage, rien de plus. Mais les transferts dans l'autre sens sont plus problématiques.
Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le ''pooling'', l'usage d'interruptions, et le ''Direct Memory Access''.
La solution la plus simple, appelée ''Pooling'', est de vérifier périodiquement si le périphérique a envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le ''Pooling'' est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.
===Les interruptions de type IRQ===
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les '''interruptions'''. Pour rappel, il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). Lors d'une interruption, le processeur suit la procédure suivante :
* arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
* exécute un petit programme nommé '''routine d'interruption''' ;
* restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
[[File:Interruption processeur.png|centre|vignette|upright=2|Interruption processeur]]
Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées '''IRQ''', sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit. Les IRQ sont générées par le contrôleur de périphérique quand c'est nécessaire.
[[File:Contrôleur de périphérique.png|centre|vignette|upright=2|Contrôleur de périphérique.]]
Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc. Pas besoin d'utiliser du ''pooling'', pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.
===Les FIFO internes au contrôleur de périphérique===
Le contrôleur de périphérique peut contenir des mémoires FIFO, afin de mettre en attente les transmissions CPU<->périphérique/bus. Rappelons que les transferts se sont entre CPU et contrôleur, puis entre contrôleur et périphérique/bus. Vu que les trois composants ne sont pas synchronisés, il est nécessaire de mettre en attente des transmissions dans les registres d'interfaçage. Mais les registres d'interfaçage ne permettent que de mémoriser une seule transmission à la fois, et ce n'est pas l'idéal niveau performance.
Par exemple, prenons l'exemple du circuit 8250 de National Semiconductors, utilisé sur les PC 8 bits. Il s'agissait d'un UART, à savoir d'un circuit qui recevait des octets envoyés sur une liaison série, connectées à un modem ou une imprimante. Il disposait d'un registre d'interface d'un octet en émission, et un autre en réception, ce qui permettait d'envoyer ou de recevoir des trames d'un octet chacune. Les bus auxquels il était relié ne gérait pas des trames plus longues, la transmission se faisait octet par octet. Le problème est que le 8250 générait une interruption processeur à chaque octet reçu !
En soi, ce n'était pas un problème si majeur, car les modems et imprimantes de l'époque étaient très lents, et que les liaisons série de l'époque avaient une fréquence minable. Mais rapidement, avec l'introduction de liaisons série plus rapide, l'UART 8250 générait trop d'interruptions et cela avait un cout en performances. Son successeur, le 16 550 UART, a corrigé ce problème en ajoutant une mémoire FIFO en sortie (et une en entrée, qu'on passe sous silence). Les octets reçus sont copiés non pas vers le registre d'interfaçage, mais vers une mémoire FIFO qui le précède. Elle est capable de mémoriser 16 octets reçus, dans leur ordre de réception. Une interruption est envoyée non pas à chaque octet, mais quand la mémoire FIFO est pleine.
Utiliser des mémoires FIFO en réception permet d'accumuler plusieurs transmissions reçues, que le processeur lit en bloc quand il est disponible. Cela permet au processeur de lire plusieurs octets d'un coup assez rapidement, plutôt que d'être dérangé pour chaque octet ou chaque trame. Ainsi, on génère moins d'interruptions. La même méthode peut s'appliquer sans interruptions, avec la technique du ''pooling'', avec des avantages similaires. Et la même chose a lieu en émission : le contrôleur peut accepter plusieurs commandes/données consécutives, et les envoyer aux périphériques une par une. L'envoi des commandes consécutives peut se faire en un seul bloc, ou bien une par une, mais avec un rythme différent de celui du périphérique.
===Un contrôleur de périphérique peut gérer plusieurs périphériques===
Les contrôleurs de périphériques les plus simples ne sont connectés qu'à un seul périphérique, via une connexion point à point. Tel est le cas du port série RS-232 ou des différents ports parallèles, autrefois présents à l'arrière des PC. Mais de nombreux contrôleurs de périphériques sont connectés à plusieurs périphériques. Prenez par exemple l'USB : vous avez plusieurs ports USB sur votre ordinateur, mais ceux-ci sont gérés par un seul contrôleur USB. En fait, ces périphériques sont connectés au contrôleur de périphérique par un '''bus secondaire''', et le contrôleur gère ce qui transite sur le bus. On devrait plutôt parler de '''contrôleur de bus''' que de contrôleur de périphérique dans ce cas précis, mais passons.
[[File:Controleur de périphérique qui adresse plusieurs périphériques.png|centre|vignette|upright=2|Contrôleur de périphérique qui adresse plusieurs périphériques]]
Les périphériques connectés à un même contrôleur peuvent être radicalement différents, même s’ils sont connectés au même bus. C'est notamment le cas pour tout ce qui est des contrôleurs PCI, USB et autres. On peut connecter en USB aussi bien des clés USB, des imprimantes, des scanners, des lecteurs DVD et bien d'autres. Mais leur respect du standard USB les rend compatibles. Au final, le contrôleur USB gère le bus USB mais se fiche de savoir s’il communique avec un disque dur, une imprimante USB ou quoique ce soit d'autre.
Toujours est-il que le contrôleur de périphérique doit pouvoir identifier chaque périphérique. Prenons par exemple le cas où une imprimante, une souris et un disque dur sont connectés en USB sur un ordinateur. Si je lance une impression, le contrôleur de périphérique doit envoyer les données à l'imprimante et pas au disque dur. Pour cela, il attribue à chaque périphérique une ou plusieurs adresses, utilisées pour l'identifier et le sélectionner. En général, les périphériques ont plusieurs adresses : une par registre d’interfaçage. L'adresse permet ainsi d'adresser le périphérique, et de préciser quel registre du contrôleur lire ou écrire. L'adresse d'un périphérique peut être fixée une bonne fois pour toutes dès la conception du périphérique, ou se configurer via un registre ou une EEPROM.
Comme on l'a vu dans le chapitre sur les bus, la sélection du bon composant se fait de deux manières : soit les périphériques vérifient si la transmission leur est dédiée, soit on utilise du décodage partiel d'adresse. Le décodage d'adresse n'est pas utilisé quand on peut ajouter ou retirer des périphériques à la demande, la première méthode est plus pratique. Le contrôleur attribue alors une adresse à chaque composant quand il est branché, il attribue les adresses à la volée. Les adresses en question sont alors mémorisées dans le périphérique, ainsi que dans le contrôleur de périphérique.
[[File:Décodage d'adresse par le contrôleur de périphérique.png|centre|vignette|upright=2|Décodage d'adresse par le contrôleur de périphérique.]]
==Les entrées d'interruption du processeur==
Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. La méthode la plus simple demande d'ajouter au processeur une entrée d'interruption, qui est mise à 1 quand une interruption survient. Cependant, la majorité des processeurs utilise deux entrées d'interruption : une pour les interruptions masquables, et une autre pour les interruptions non-masquables.
===L'entrée d'interruption : niveaux logiques ou fronts===
Nous allons d'abord nous intéresser aux cas d'une interruption matérielle unique, c'est à dire au cas avec un seul périphérique. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l''''entrée d'interruption''', souvent notée INTR ou INT.
L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d''''entrée déclenchée par niveau logique''' et d''''entrée déclenchée par front montant/descendant'''. Les noms sont barbares mais recouvrent des concepts très simples.
Le plus simple est le cas de l'''entrée déclenchée par niveau logique'' : la mise à 1 de cette entrée déclenche une interruption au cycle d'horloge suivant. En réalité, la majorité des processeurs préfèrent mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.
Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée, qui demande de communiquer avec le contrôleur de périphérique.
Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.
A l'opposé, avec une ''entrée déclenchée par front montant/descendant'', on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.
Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.
===Les deux entrées d'interruption : masquable et non-masquable===
Pour rappel, le masquage d'interruption permet de retarder ou d'ignorer les interruption tant que le masquage est actif. Les interruptions ignorées/retardées sont dites, masquées comme le veut la terminologie. Le masquage des IRQ s'implémente au niveau du processeur, au niveau de l'entrée d'interruption. En cas de masquage des interruptions, il ignore simplement ce qu'il y a sur l'entrée d'interruption. Mais il doit exécuter l'interruption une fois le masquage levé, ce qui demande de mémoriser qu'une interruption a eu lieu.
Pour cela, l'entrée d'interruption masquable contient une bascule qui est mise à 1 quand l'entrée d'interruption passe à 1. Elle est remise à 0 quand le processeur a pris en compte l'interruption et l'a exécutée. De plus, en sortie de la bascule, le processeur ajoute une porte ET pour combiner le contenu de la bascule avec un signal généré par le séquenceur qui dit s'il faut ou non masquer l'interruption
Les '''interruptions non-masquables''' ne doivent pas être masquées, quelle que soit la situation. Les interruptions non-masquables sont généralement générées en cas de défaillances matérielles graves, qui demandent une intervention immédiate du processeur. Par exemple : une surchauffe du processeur, une défaillance de l'alimentation électrique, une erreur de parité mémoire, etc. Le résultat de telles défaillances est que l'ordinateur est arrêté/redémarré de force, ou alors affiche un écran bleu. Elles peuvent être générées par un contrôleur de périphérique, ou par des circuits placés sur la carte mère comme un watchdog timer, des circuits de détection de défaillances matérielles, des circuits de contrôle de parité mémoire, etc.
Si on omet les défaillances matérielles, les interruptions non-masquables servent pour la gestion du watchdog timer, vu il y a quelques chapitres. Pour rappel, le watchdog timer est un mécanisme de sécurité qui redémarre l'ordinateur s'ils suspecte que celui-ci a planté. Le watchdog timer est un compteur/décompteur qui redémarre le système s'il déborde. Mais une interruption non-masquable réinitialise le watchdog timer régulièrement, ce qui signifie que le système n'est pas censé redémarrer.
Pour gérer les interruptions non-masquables, beaucoup de processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables. La différence entre les deux est l'usage ou non de la bascule mentionnée plus haut. L'entrée d'interruption normale dispose de cette bascule pour le masquage, alors que l'entrée d'interruption non-masquable ne l'a pas.
==Le contrôleur d'interruption==
Précédemment, nous avons vu le cas où nous n'avons qu'un seul contrôleur de périphérique dans l’ordinateur. Mais avec plusieurs périphériques, l'implémentation des interruptions matérielles est plus compliqué.
Dans une implémentation simple des IRQ, chaque contrôleur de périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher une entrée d'interruption par périphérique, ce qui limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.
[[File:Entrées d'interruptions séparées pour chaque périphérique.png|centre|vignette|upright=2|Entrées d'interruptions séparées pour chaque périphérique]]
Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.
[[File:Entrée d'interruption partagée.png|centre|vignette|upright=2|Entrée d'interruption partagée]]
Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un ''numéro d'interruption'' qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de <math>N</math> entrées à <math>log_2{(N)}</math> entrées. Le processeur récupère ce numéro d'interruption, qui est généré à l'extérieur du processeur.
Pour implémenter cette solution, on a inventé le '''contrôleur d'interruptions'''. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption, souvent mappé en mémoire. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption, qui permet au processeur de lire le numéro d'interruption.
[[File:Contrôleur d'interruptions IRQ.png|centre|vignette|upright=3|Contrôleur d'interruptions IRQ]]
L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence à priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.
===La contrôleur d'interruption est une entrée-sortie comme une autre===
Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Pour cela, le contrôleur d'interruption est techniquement traité comme n'importe quel périphérique. Le registre pour le numéro d'interruption est simplement mappé en mémoire RAM, à savoir qu'il est lisible à une adresse bien précise. Et on peut aller plus loin : tous les registres du contrôleur d'interruption sont mappés en mémoire, le contrôleur d'interruption est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.
Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Une autre solution connecte le contrôleur d'interruption sur le bus système. Les deux solutions sont des détails d'implémentation. Mais dans les deux cas, le contrôleur d'interruption doit être connecté sur le processeur, sur son entrée d'interruption au minimum, via des fils dédiés. Le résultat est un mélange entre bus dédié et connexion au bus système/IO. Nous verrons cela avec l'exemple qui va suivre.
Pour rentrer dans le détail, étudions le cas du contrôleur d'interruption 8259 couplé à un processeur Intel 486. L'Intel 8259 était un contrôleur d'interruption utilisé dans les premiers PC. Il était présent sur toutes les cartes mères des PC de l'époque, il a vraiment eu son heure de gloire. Il gérait 8 interruptions, grâce à 8 entrées IRQ et une sortie d'interruption. C'est peu, mais laissons cela de côté pour le moment. Il était connecté sur le bus de données du processeur, qui servait de bus système.
En plus des 8 entrées IRQ, et des entrées CAS qu'on laisse de côté pour le moment, il disposait de broches spécialisées pour communiquer avec le processeur.
* La sortie INTR permettait d'envoyer une interruption au processeur.
* L'entrée INTA (''Interrupt Acwnoledge'') permettait au processeur de dire qu'il avait bien pris en compte l'interruption, ce qui permettait au 8259 de remettre sa sortie INTR à zéro.
* Les deux signaux RD et WR permettaient au processeur de lire/écrire les registres du 8259. La broche WR était mise à 0 quand le processeur envoyait une commande au 8259, RD était mis à 0 quand il lisait ses registres.
* Un bus de 8 bits permettait de connecter le contrôleur d'interruption au bus de données.
Le 8259 était traité comme une entrée-sortie tout ce qu'il y a de plus banale. Il était connecté sur le bus système, au même titre que les entrées-sorties, la mémoire, etc. Il n'y avait pas de bus dédié, au-delà des broches INTR, INTA. Il est donc connecté aux circuits qui administrent le bus système, qui est lui-même relié au processeur.
Vu que ses registres étaient mappés en mémoire RAM, il était relié au circuit décodeur d'adresse, que nous verrons en détail dans le prochain chapitre. Pour rappel, ce décodeur d'adresse connecte/déconnecte les entrées-sorties du bus, suivant les adresses envoyées dessus. Si l'adresse envoyée est destinée à la RAM ou une ROM, les entrées-sorties sont déconnectées du bus, en mettant à zéro leur signal CS (''Chip Select''). Si l'adresse est destinée à une entrée-sortie, elle est connectée sur le bus, en mettant son signal CS à 1. Le 8258 avait une entrée CS connectée directement à ce décodeur d'adresse, comme toutes les autres entrées-sorties. Il avait aussi une entrée d'adresse de un bit, qui permettait d'adresser les registres internes au 8259.
Notons que le bus dédié était un bus de 8 bits, alors que les processeurs Intel de l'époque étaient des processeurs 16 ou 32 bits. Leur bus de données faisait donc 16 ou 32 bits, ce qui ne collait pas avec les 8 bits du 8259. Il y avait donc un circuit pour faire l'interface entre les deux, qui est représenté ci-dessous avec deux circuits : une mémoire tampon de 32 bits et un circuit de commande de ''byte swap''.
[[File:Intel486 with 82C59A.png|centre|vignette|upright=2|Intel486 with 82C59A]]
===Les contrôleurs d'interruption en cascade===
Un contrôleur d'interruption permet de gérer un nombre limité d'interruptions. Par exemple, l'ancien Intel 8259 gérait 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Même pour l'époque, ce n'était pas assez. Un PC contenait plus de 8 périphériques, en tenant compte de la ''real time clock'' et d'autres composants intégrés sur la carte mère. Aussi, il fallait trouver une solution pour gérer plus de 8 interruptions. Et la solution a été d'utiliser plusieurs contrôleurs d'interruption en cascade, à savoir que le second contrôleur d'interruption émettait une interruption vers le premier.
C'était le cas avec les premiers processeurs d'Intel dès le 8086, notamment le 486, utilisaient deux contrôleurs Intel 8259 : un contrôleur maitre, un contrôleur esclave. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, alors que le contrôleur maitre gérait le reste. Leur sortie d'interruption était soit connectée au processeur (pour le contrôleur maître), soit au contrôleur maître (pour le contrôleur esclave). Le tout permettait de gérer 15 interruptions : 8 interruptions pour le contrôleur esclave, 7 pour le contrôleur maitre (une des entrées était prise par le contrôleur esclave).
Le fonctionnement était le suivant. Si une interruption avait lieu en dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Le processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.
[[File:Intel486 and cascading PIC.png|centre|vignette|upright=2|Contrôleurs d'interruptions IRQ du 486 d'Intel.]]
[[File:Intel 8259.svg|vignette|Intel 8259]]
En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclaves et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.
Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 7, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.
Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.
Le fait de mettre en cascade deux 8259 a fonctionné pour les PC avec un bus ISA. Mais avec l'introduction du bus PCI, les choses sont devenues bien plus complexes. Le nombre de périphériques, et donc d'interruptions, a grimpé en flèche. Le même problème de manque d'interruption s'est fait sentir, et la solution a été similaire : rajouter un contrôleur d'interruption en cascade. Sauf que pour le bus PCI, on n'a pas rajouté un 8259, mais un autre contrôleur d'interruption dédié au bus PCI, appelé le PIR (''Programmable Interrupt Request''). Ce contrôleur d'interruption émettait 4 fils vers le 8259 esclave, connectés aux entrées d'IRQ numéro 5, 9, 10, et 11.
===Les interruptions sur les systèmes multicœurs et multi-processeurs===
L'arrivée des systèmes avec plusieurs processeurs, plusieurs cœurs, a demandé d'adapter les contrôleurs d'interruption. Et en plus d'adapter le tout aux systèmes multicœurs, il a été décidé de simplifier les contrôleurs d'interruption, en réduisant leur mise en cascade. Pour cela, les deux 8259 ont été remplacés par un unique contrôleur d'interruption, appelé l''''APIC''' (''Advanced PIC''). Je dis un contrôleur d'interruption, mais l'APIC est en réalité un standard qui peut être respecté par des contrôleurs d'interruption très différents. Si les plus simples se contentent de remplacer deux 8259 en cascade, d'autres vont plus loin et intègrent carrément un PIC pour le bus PCI !
Les tout premiers APIC avaient 16 entrées d'interruption et se contentaient de remplacer à la lettre les deux 8259. Le PIC pour le bus PCI était toujours présent. Par la suite, d'autres APIC sont apparus, avec 24 ou 32 entrées d'interruption. Typiquement, les 16 premières entrées d'interruptions étaient réservées pour remplacer/émuler les deux 8259, le reste était utilisable avec plus ou moins de flexibilité. Par exemple, un APIC avec 32 entrées d'interruption peut utiliser 16 entrées rien que pour le bus PCI, ce qui permet parfois de se passer de PIC. Un exemple d'APIC était le 82093AA. Il disposait de 24 entrées d'IRQ : 13 pour le bus ISA, 4 pour le bus PCI, une entrée d'interruption pour mettre des contrôleurs d'interruption en cascade, le reste était plus ou moins spécialisé.
Le standard impose la présence de plusieurs contrôleurs d'interruptions. Chaque cœur/processeur a son propre contrôleur d'interruption, avec un contrôleur général. Le contrôleur d'interruption général est nommé ''IO-APIC'', les autres sont les APIC locaux (''local-APIC''). L'''IO-APIC'' reçoit les interruptions provenant des périphériques, et les redirige vers les processeurs/cœurs adéquats. Pour cela, sont tous reliés soit par un bus APIC dédié, soit par le bus système ou un bus existant.
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|vignette|upright=1.5|Contrôleurs d'interruptions sur systèmes x86 multicœurs.]]
Le premier APIC était le 82489DX. Il était prévu pour les systèmes avec deux cœurs, deux processeurs. Il regroupait l'IO-APCI et deux APIC locaux en un seul circuit. Mais cette solution a ensuite été modifiée, les APIC locaux ont été intégrés aux processeurs/cœurs eux-mêmes, ne laissant que l'IO-APCI sur la carte mère, dans son ''chipset''. Le standard APIC a évolué pour donner l'xAPIC et le x2APIC. La différences principales sont que le nombre de processeurs/cœurs supporté est passé respectivement à 256, puis à 2^32.
===Les ''Message Signaled Interrupts''===
Les interruptions précédentes demandent d'ajouter un fil par périphérique, pour que celui-ci signale une interruption au contrôleur d'interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande de relier 5 fils au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.
Il existe cependant un type d'interruption qui permet de se passer de ces fils d'interruptions : les '''interruptions signalées par message''', ''Message Signaled Interrupts'' (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens. Aussi, nous allons prendre l'exemple du bus PCI Express dans ce qui suit, même si les explications fonctionnent pour d'autres bus.
Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une ''adresse réservée''. Le contrôleur d'interruption surveille en permanence ce qui est transmis sur le bus PCI Express. Dès qu'il voit passer une écriture à cette adresse réservée, il déclenche une interruption sur le processeur. Le reste du traitement de l'interruption est le même qu'avec les IRQ. Le processeur ayant reçu l'interruption communique alors avec le contrôleur d'interruption pour savoir quel périphérique a déclenché une interruption, récupérer le numéro de l'interruption, etc.
: Le processeur a toujours une entrée d'interruption unique reliée, reliée au contrôleur d'interruption, ce sont les fils entre périphérique et contrôleur d'interruption qui disparaissent.
Une implémentation possible place les adresses réservées dans le contrôleur d'interruption. Les adresses réservées correspondent à des registres du contrôleur d'interruption, qui sont mappés en mémoire. Toute écriture dans cette adresse réservée modifiera donc un registre du contrôleur d'interruption. La détection de l'écriture dans une adresse réservée est donc particulièrement simple.
Il y a une adresse réservée par interruption, ce qui fait que le contrôleur d'interruption détermine le numéro de l'interruption à partir de l'adresse réservée utilisée/écrite. Le périphérique écrit un message dans l'adresse réservée, qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations.
Un avantage est que cela permet de gérer un grand nombre d'interruptions assez facilement. On n'est plus limité par un nombre de fils, la limite tient dans le nombre d'adresses réservées. Et les controleurs d'interruptions peuvent facilement gérer un grand nombre d'adresse, tant qu'ils ont le hardware pour. L'ancien PCI 2.2 gère jusqu'à 32 adresses réservées, le bus PCI 3.0 MSI-X alloue 2048 adresses réservées. En comparaison le bus PCI gérait maximum 4 IRQ câblées, du fait des limitations en termes de fils (deux fils maximum).
Un avantage lié au précédent est que cela simplifie la gestion des interruptions pour le processeur. Les 4 interruptions du bus PCI étaient partagées entre périphériques PCI, ce qui fait que le processeur avait du mal à déterminer quel périphérique avait lancé une interruption. Quand deux périphériques PCI partageaient une interruption, le processeur avait du mal à savoir lequel des deux lançait l'interruption. Mais avec les MSI, on a assez d'interruption pour ne pas avoir à les partager entre périphériques. Chaque périphérique a ses interruptions, ses adresses réservées, il ne marche pas sur les pieds des autres périphériques.
Un autre avantage est que les interruptions passent par le bus PCI Express. La conséquence est que sur les systèmes multicoeurs/multi-processeurs, il n'y a pas besoin d'utiliser de contrôleur d'interruption général, d'IO-APIC. La présence d'APIC locaux est toujours nécessaire, mais l'IO-APIC n'est gardé que par souci de compatibilité avec les vieilles interruptions PCI.
===Les généralités sur les interruptions matérielles===
Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent implémentées par le contrôleur d'interruption. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver.
En premier lieu, voyons pourquoi il est pertinent de prioriser certaines interruptions plutôt que d'autres. Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Pour gérer des interruptions simultanées, un '''système de priorité d'interruption''' est mis en place, où certaines interruptions sont prioritaires sur les autres. Par exemple, l'interruption qui gère l'horloge système est prioritaire sur les interruptions en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.
La gestion des priorités est gérée par le contrôleur d'interruption, ou par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.
Ensuite, il faut aussi parler du '''masquage des interruptions''', qui pour rappel, permet de désactiver temporairement les interruptions. Si le masquage est temporairement activé, les interruptions sont soit ignorées, soit mise en attente et exécutées une fois le masquage levé. Le masquage peut être total, dans le sens où toutes les interruptions masquables sont masquées en bloc, ce qui revient à désactiver les interruptions (sauf les non-masquables). On parle alors de masquage global. Mais il est aussi possible de ne masquer qu'une partie des interruptions masquables. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du ''watchdog timer'', alors que les deux sont masquables. On parle alors de '''masquage d'interruption sélectif'''. L'avantage est que cela permet de simplifier l'implémentation des interruptions masquables et non-masquables.
Le contrôleur d'interruption gère de lui-même le masquage des interruptions. Dans le cas le plus simple, où on masque toutes les interruptions en bloc, il suffit d'ajouter un '''bit MASK''' dans le contrôleur d'interruption, qui dit si les interruptions sont masquées ou non. La valeur du bit est combinée avec les signaux d'interruptions pour calculer le signal à envoyer sur l'entrée d'interruption finale. Notons que le contrôleur n'utilise ce bit que pour les interruptions non-masquables. Il dispose de deux sorties : une pour l'entrée d'interruption normale, l'autre pour l'entrée d'interruption masquable. Le bit MASK n'agit que sur la sortie pour les interruptions masquables, pas l'autre.
[[File:Masquage global des interruptions dans le controleur d'interruption.png|centre|vignette|upright=2|Masquage global des interruptions dans le controleur d'interruption]]
Masquer les interruptions individuellement demande quelques adaptations. Mais l'idée est globalement d'avoir un bit MASK pour chaque interruption, de le dupliquer en autant d'interruption masquables existantes, et de les regrouper dans un registre. Pour cela, le contrôleur d'interruption (ou le processeur) disposent d'un registre appelé le '''registre de masque d'interruption'''. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.
==Le ''Direct memory access''==
Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le ''bus mastering'', qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.
Le '''Direct Memory Access''', ou DMA, est une technologie de ''bus mastering'' qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.
Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.
===Le contrôleur DMA===
Avec le DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial : le '''contrôleur DMA'''. Il est généralement intégré au contrôleur de périphérique et placé sur la carte mère, parfois intégré au périphérique, parfois intégré au processeur, mais est toujours connecté au bus mémoire.
Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.
Le contrôleur DMA contient généralement plusieurs compteurs : un pour l'adresse de la source, un pour l'adresse de destination, un autre pour le nombre de bytes restants à copier. Les deux compteurs d'adresse sont initialisé avec l'adresse source/destination de départ, à savoir l'adresse du bloc à copier et celle du bloc de destination. Elle est incrémenté ou décrémentée à chaque envoi de données sur le bus. Le compteur de ''byte'' restant est décrémenté à chaque copie d'un mot mémoire sur le bus. Le compteur de bytes restant est purement interne au contrôleur mémoire, alors que le contenu des compteurs d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.
[[File:DMA controller transfer count register.jpg|centre|vignette|upright=2.5|DMA controller transfer count register]]
[[File:DMA controller memory address registers.jpg|centre|vignette|upright=2.5|DMA controller memory address registers]]
Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore.
[[File:Controleur DMA.png|centre|vignette|upright=2.5|Controleur DMA]]
Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le contrôleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en place quand le périphérique et la mémoire n'ont pas la même vitesse.
===Les modes de transfert DMA===
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode ''block'', le mode ''cycle stealing'', et le mode transparent.
Dans le '''mode block''', le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !
Dans le '''mode cycle stealing''', on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.
Et enfin, on trouve le '''mode transparent''', dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.
===Les limitations en termes d’adressage===
Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.
La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.
Typiquement, le contrôleur DMA ne gère que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.
La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un '''tampon DMA''' ou encore un ''bounce buffer''. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.
===Les limitations en termes d’alignement===
La seconde limitation des contrôleurs DMA est qu'ils ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 16, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.
À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.
Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.
===Le 8237 et son usage dans les PC===
Le 8237 d'Intel était un contrôleur DMA (''Direct Memory Access'') présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 16 bits, c'est-à-dire manipulait les données sur 16 bits, mais utilisait des adresses sur 20 bits (adressage segmenté, en utilisant deux registres 16 bits, dont un registre de segment décalé de 4 bits). Le processeur 8088 était identique au 8086 mais utilisait un bus de données 8 bits (les instructions accédant la mémoire ou l'interface d'entrée-sortie sur 16 bits prenant des cycles supplémentaires car exécutées en deux temps : octet de poids faible, puis octet de poids fort), il avait lui aussi un bus d'adresse sur 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empêché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.
====L'usage du 8237 sur les premiers IBM PC====
Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (''bank switching''), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit, car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 bits ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA ou page DMA.
Un défaut de cette solution est que le contrôleur DMA gère 16 pages de 64 Kibioctets, au lieu d'une seul de 1 Mébioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une page à une autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une page. Mais si jamais les 3 premiers Kb sont dans une page, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la page, et la copie reprend au tout début de la page de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.
Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 '''canaux DMA''', qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.
====Les deux 8237 des bus ISA====
Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets (page sur 8 bits), mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 maître pour gérer le 8237 esclave. Le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, cela libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :
Premier 8237 (esclave) :
* Rafraichissement mémoire ;
* Hardware utilisateur, typiquement une carte son ;
* Lecteur de disquette ;
* Disque dur, port parallèle, autres ;
Second 8237 (maitre) :
* Utilisé pour mise en cascade ;
* Disque dur (PS/2), hardware utilisateur ;
* Hardware utilisateur ;
* Hardware utilisateur.
Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.
===L'usage du DMA pour les transferts mémoire-mémoire===
Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.
Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs coprocesseurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).
Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un ''local store'' dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce ''local store'', rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les ''local stores''. Chaque SPE incorporait son propre contrôleur DMA.
[[File:Schema Cell.png|centre|vignette|upright=2|Schema du processeur Cell]]
===DMA et cohérence des caches CPU===
Les transferts DMA se font entre la RAM et un périphérique, et les deux sens de transfert sont possibles. Certains périphériques se contentent d'un seul sens de transfert. Par exemple, pour une carte son, les transferts se font de la RAM vers la carte son, l'autre sens se résume à des interruptions. Le transfert DMA envoie les données sonores vers la carte son qui se charge d'envoyer le tout sur les hauts-parleurs ou un casque. Mais d'autres périphériques font des transferts dans l'autre sens, du périphérique vers la RAM. Pensez à une carte réseau : les téléchargements impliquent que les données reçues par la carte réseau sont copiées en RAM, alors que c'est l'inverse pour l'''upload''.
Et les transferts DMA dans le sens périphériques -> RAM posent un problème sur les architectures avec une mémoire cache. De tels transferts peuvent modifier des adresses qui sont mises en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Le cache contient alors une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multicœurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.
Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.
Différentes technologies matérielles ont été inventées pour éliminer le problème à la source. L'une d'entre elles était la technologie ''Data Direct I/O'' d'Intel. Elle permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.
===La cohérence des caches périphériques===
Un problème similaire au précédent se manifeste sur les périphériques qui incorporent une mémoire cache. Nous parlons ici de périphériques qui n'ont pas de mémoire RAM interne, et dont le cache contient des copies de données présentes en RAM système, en RAM principale. Nous appellerons de tels caches des '''caches périphériques''', sous entendu intégrés à un périphérique. Il est important de préciser que de tels caches peuvent maintenir des copies de données en RAM système. Les caches ne pouvant pas contenir de données provenant de la RAM système ne sont pas concernés par ce qui suit, ce sont des caches purement internes au périphérique.
[[File:Cache périphérique.png|centre|vignette|upright=2|Cache périphérique]]
Certaines cartes réseaux incorporent de tels caches, pour améliorer les performances lors des transferts réseaux. Le cache périphérique permet alors soit précharger les données à uploader depuis la RAM, soit en accumulant les données téléchargées avant de tout envoyer d'un seul bloc dans la RAM. Mais précisons que seules les cartes réseaux de haute performance, destinées aux serveurs et ''mainframes'', intègrent des caches périphériques. Les cartes réseaux des PC n'ont pas de cache périphériques de ce type, elles se contentent de mémoire tampon de type FIFO qui ne sont pas des caches.
Avec de tels périphériques, il arrive que le processeur modifie des données en RAM, alors qu'une copie est dans le cache périphérique. Il faut alors que l'écriture en RAM soit propagée dans le cache périphérique. Et cela demande là encore d'utiliser des mécanismes de cohérence des caches.
La solution la plus simple est d'invalider le cache périphérique avant un transfert DMA, ou avant toute écriture en mémoire RAM qui peut poser problème. Typiquement, c'est le pilote du périphérique qui s'en occupe. Il réserve quelques portions de RAM pour communiquer avec le périphérique, et toute écriture dedans entrainera une invalidation. Le périphérique doit gérer nativement des commandes d'invalidation, qui ordonnent au périphérique d’invalider son cache périphérique. Le pilote du périphérique envoie ces commandes quand la situation l'exige.
La technologie CXL (''Compute Express Link'') permet d'aller plus loin, en ajoutant un protocole de cohérence des caches au PCI Express. Le protocole CXL est une surcouche au PCI Express, qui est gérée par le processeur. Plus précisément, CXL réutilise la couche physique du protocole, mais change les couches transport et liaison. Concrètement, cela signifie que les interconnexions série du PCI Express sont réutilisables telles quelles par CXL, ce qui est l'idéal niveau compatibilité matérielle.
Elle réutilise des mécanismes de cohérence des caches utilisés sur les processeurs multicœurs, qui seront décrits dans un chapitre ultérieur (un protocole de type MESI, pour rentrer dans les détails). L'intérêt est que les caches ne sont pas invalidés à chaque écriture problématique, la donnée adéquate est simplement mise à jour. De plus, la mise à jour et la détection des écritures fautives ne fait pas intervenir le pilote de périphérique, tout est géré par le processeur lui-même.
Mais CXL va au-delà de la simple cohérence des caches périphériques. Il permet aussi au processeur d'adresser de la mémoire RAM installée sur un périphérique, à travers une liaison PCI Express. Toujours est-il que CXL est un regroupement de trois protocoles distincts, nommés CXL.io, CXL.cache, et CXL.memory. CXL.io gère des transferts DMA, la mémoire virtuelle du périphérique, les interruptions, la découverte des périphériques CXL, leur configuration, la gestion des erreurs, etc. CXL.cache gère la cohérence des caches périphériques, il permet à un périphérique d’accéder et de cacher la RAM système. Enfin, CLX.mem permet au processeur d'adresser la mémoire RAM du périphérique.
Il faut noter que chaque périphérique implémente CXL a sa sauce. Il doit au minimum implémenter CXL.io, mais fait ce qu'il veut pour CXL.cache et CXL.mem. Et cela permet de classer les périphériques CXL en trois types.
* Le type 1 correspond aux périphériques sans RAM intégrée, avec un cache périphérique. Ils peuvent se passer de CXL.mem, mais pas de CXL.cache.
* Le type 2 correspond aux GPU et autres cartes accélératrices, qui ont une RAM intégrée qui gagne à être adressée directement par le processeur. Dans ce cas, il faut implémenter CXL.io, CXL.cache, et CXL.memory.
* Le type 3 correspond à des ''RAM drive'', des systèmes permettant d’ajouter de la RAM à l'ordinateur sans ajouter de RAM système. La RAM ajoutée est placée sur un périphérique PCI Express, mais sert de complément à la RAM principale. l'idée est que les données sont déplacées entre RAM système et RAM du périphérique selon les besoins, la RAM du périphérique servant de second ''swapfile'' de la mémoire virtuelle. La cohérence des caches n'est alors pas nécessaire, CXL.cache n'est pas utilsié, alors que CXL.mem l'est.
===L'interaction entre DMA et mémoire virtuelle : l'IO-MMU===
Le contrôleur DMA est initialisé par le processeur avec des adresses de départ. Mais que se passe-t-il si le processeur utilise la mémoire virtuelle, l'abstraction mémoire ? Dans ce cas, le processeur initialise le contrôleur DMA avec des adresses virtuelles, ce qui pose problème. Pour éviter cela, les contrôleurs DMA modernes incrémentent des adresses virtuelles nativement, mais les traduisent en adresse physique avant tout accès à la RAM. En clair, les contrôleurs DMA incorporent une '''IO-MMU''', un circuit qui traduit les adresses virtuelles en adresses physiques. Elle est similaire à la MMU du processeur, si ce n'est qu'elle est intégrée dans le contrôleur DMA.
La IO-MMU est intégrée dans le contrôleur DMA, qui peut être dans le ''chipset'' de la carte mère ou dans un périphérique. Si elle est dans le ''chipset'', elle est partagée entre plusieurs périphériques. Mais il n'est pas rare que la IO-MMU soit intégré dans un périphérique. L'exemple type est celui des cartes graphiques AGP et PCI-Express, qui utilisaient une IO-MMU standardisée, appelée la ''Graphics address remapping table''. La première carte graphique NVIDIA, la NV1, utilisait un système similaire, comme illustré ci-dessous.
[[File:Microarchitecture du GPU NV1 de NVIDIA.png|centre|vignette|upright=2|Microarchitecture de la carte graphique NV1 de NVIDIA]]
Évidemment, la IO-MMU a sa propre table des pages, qui est gérée par le pilote de périphérique, qu'on nommera '''table des pages IO'''. La IO-MMU intègre aussi des ''page table walkers'', des TLBs et toutes les optimisations associées. La table des pages de l'IO-MMU est séparée des autres tables des pages, bien que certaines optimisations permettent de fusionner les deux. Placer la table des pages IO en mémoire RAM fonctionne très bien pour les contrôleurs DMA intégré au ''chipset'' de la carte mère, voire ceux dans le processeur lui-même. Mais les périphériques qui incorporent un contrôleur DMA font autrement.
Avec une table des pages IO en RAM, le périphérique devrait y accéder à travers le bus. Par exemple, une carte graphique devrait passer par le bus PCI-Express à chaque accès à la table des pages IO. Les performances seraient catastrophiques, même si on peut limiter la casse avec une TLB de grande taille. En pratique, le périphérique incorpore de la mémoire RAM et met la table des pages IO dedans. Par exemple, les cartes graphiques incorporent une mémoire vidéo de grande taille, dont elles peuvent réserver une petite partie pour la table des pages. La carte graphique NV1 de NVIDIA faisait autrement : elle mettait la table des pages dans une mémoire dédiée.
[[File:IO-MMU et table des pages sur périphérique avec RAM locale.png|centre|vignette|upright=2|IO-MMU et table des pages sur périphérique avec RAM locale]]
L'usage de l'IO-MMU permet de passer outre les limitations d'adressage du contrôleur DMA. Le contrôleur DMA peut gérer des adresses virtuelles très courtes, qui sont traduites en adresses physiques longues. Il s'agit d'un cas particulier d'extension d'espace d'adressage, qui porte le nom de '''''DMA Remapping'''''. Le résultat est que l'on peut se passer de tampons DMA, des ''bounce buffer''. Les données à envoyer au périphérique peuvent être placées n'importe où en mémoire physique, le contrôleur DMA utilisera des adresses physiques assez longues pour les adresser.
Un autre avantage est que l'on peut communiquer avec un périphérique DMA sans avoir à allouer des blocs de mémoire contiguë. Avec la pagination, un bloc de mémoire transféré via DMA peut être éclaté sur plusieurs pages séparées et dispersées en mémoire RAM. Le bloc de mémoire est contigu dans l'espace d'adressage, mais éclaté en mémoire physique.
Un autre avantage est la protection mémoire. Avec une IO-MMU adéquate, les échanges entre processeur et périphérique sont sécurisés. La mémoire virtuelle des périphériques incorpore des mécanismes de protection mémoire. Grâce à eux, le périphérique ne peut pas accéder à des régions de la mémoire auquel le pilote de périphérique ne lui a pas donné accès. Une erreur de configuration ou de programmation ne permet pas au périphérique d'accéder à des régions mémoire protégées, qui ne lui sont pas allouées. Des tentatives de hacking basée sur des attaques DMA ne marchent plus avec ce type de mémoire virtuelle.
Un dernier avantage de l'IO-MMU se manifeste sur les périphériques qui incorporent de la mémoire RAM. Un bon exemple est celui des cartes graphiques dédiées, qui incorporent actuellement plusieurs gibi-octets de mémoire vidéo. Pour éviter toute confusion, nous allons parler de '''mémoire locale''' pour la RAM dans le périphérique, de '''mémoire système''' pour la RAM de l'ordinateur. L'IO-MMU permet d'adresser plus de RAM qu'il n'y a de RAM locale, le surplus d'adresse étant pris dans la RAM système. C'est un peu l'équivalent de la mémoire virtuelle, sauf qu'au lieu d'utiliser un fichier ''pagefile'' sur le disque dur, on utilise de la RAM système. Par exemple, si on prend une carte graphique avec 6 gigas de RAM dédiée, elle pourra gérer jusqu'à 8 gigas de RAM : les 6 en mémoire vidéo, plus 2 gigas fictifs en mémoire système.
[[File:Mémoire virtuelle des cartes graphiques dédiées.png|centre|vignette|upright=2|Mémoire virtuelle des cartes graphiques dédiées]]
==Le ''chipset'' de la carte mère==
Pour résumer ce chapitre, la communication avec les périphériques demande beaucoup de composants matériels. Un ordinateur moderne contient plusieurs dizaines, si ce n'est des centaines, de contrôleurs de périphériques. Par exemple, un ordinateur de type PC contient des contrôleurs pour le bus USB, un pour la carte son, un autre pour le disque dur, un autre pour les péripéhriques PCI Express, et ainsi de suite. Et il faut aussi rajouter un contrôleur d'interruption, éventuellement un contrôleur DMA, pour que les performances soient dignes de ce nom. Auparavant, le contrôleur d'interruption et le contrôleur DMA étaient des circuits intégrés séparés, soudés sur la carte mère. Mais ce n'est plus le cas de nos jours.
===Les ''chipsets'' sont des regroupements de circuits très divers===
De nos jours, tout est intégré dans un circuit unique, appelé le ''chipset'' de la carte mère. Il intègre un contrôleur DMA, le contrôleur d'interruption, ainsi que de nombreux contrôleurs de périphériques. Par exemple, les contrôleurs USB sont directement intégrés dans le ''chipset'', idem pour les contrôleurs S-ATA/nVME et PCI Express.
[[File:Chipset et contrôleurs de périphériques.png|centre|vignette|upright=2|Chipset et contrôleurs de périphériques]]
Dire ce que regroupe un ''chipset'' est donc compliqué, car le contenu du ''chipset'' dépend fortement de la carte mère. Le résumer en un simple regroupement de circuits aux fonctions disparates est de loin la meilleure définition possible. Comme le disent si bien les anglais : "A chipset is a set of chips".
Le regroupement de tout ces circuits dans un ''chipset'' est une conséquence de la miniaturisation des transistors. Souder une dizaine de contrôleurs de périphériques différents sur la carte mère, ce qui avait un coût en termes de branchements, de coût de production et autres. Mais la miniaturisation a permis de regrouper le tout dans un seul boîtier. Des composants autrefois soudés sur la carte mère, comme des ''timers'', la CMOS RAM ou la ''Real Time Clock'' peuvent parfois être intégrés aux ''chipset''. Et il intègre souvent le microcontrôleur de surveillance matérielle, qui surveille les températures et tensions, pour contrôler les ventilateurs et les régulateurs de voltage.
Il faut aussi noter que les ''chipsets'' modernes servent aussi de répartiteurs, qui connectent entre eux CPU, RAM, IO et périphériques. Mais nous ne reparlerons pas de cela ici, c'est quelque chose qui est plus pour le prochain chapitre. Et nous en avions déjà parlé longuement dans le chapitre sur l'architecture de base d'un ordinateur.
[[File:IO mappées en mémoire avec séparation des bus.png|centre|vignette|upright=2|IO mappées en mémoire avec séparation des bus, usage d'un répartiteur]]
===Un exemple de ''chipset'' pour le bus ISA===
Pour donner un exemple de ''chipset'' de ce genre, nous allons regarder du côté des anciens PC. Les premiers PC utilisaient un bus système unique, sur lequel tout était connecté. Le bus en question s'appelait le '''bus ISA''', pour ''Industry Standard Architecture''. De nombreux composants présents sur la carte mère étaient connectés sur ce bus ISA. La liste suivante, non-exhaustive, contient les composants principaux :
* un contrôleur mémoire pour DRAM ;
* le contrôleur de bus 8288 ;
* un contrôleur d'interruptions 8259 et un contrôleur DMA 8237
* un contrôleur d'interface parallèle 8255 ;
* un générateur d'horloge 8284 ;
* une ''Real Time Clock'' pour gérer la date et l'heure ;
* le ''Programmable Interval Timer'' (le fameux 8254 qu'on a vu dans le chapitre sur les ''timers'') ;
* le contrôleur de clavier XT ;
* l'Intel 82091AA, qui était connecté au lecteur de disquette, au port série et au port parallèle (deux connecteurs présents à l'arrière des vieux PC).
[[File:Architecture de l'IBM PC compatible.png|centre|vignette|upright=2.5|Architecture de l'IBM PC compatible]]
Un des premier ''chipset'' inventé pour les PC a été le 82C100. Il était prévu pour fonctionner avec un processeur Intel x86, un 286 pour être précis. Il combine le contrôleur de clavier avec les composants de la liste précédentes, à l'exception de la ''Real Time Clock''.
Son successeur est le 82C206 ''Integrated Peripheral Controller'' (IPC). Les différences avec le précédent sont mineures. La ''Real Time Clock'' et la CMOS RAM sont maintenant intégré dans le ''chipset''. Le contrôleur DMA et le contrôleur d'interruption sont doublés : il y en a deux exemplaires. Par contre, le contrôleurs de clavier et d'interface parallèle 8255 disparaissent.
Les deux ''chipsets'' ont été développés par l'entreprise Chips and Technologies. Ils étaient composés de quatre à cinq circuits intégrés distincts, donc 4 boitiers. Sur l'image suivante, vous pouvez voir les quatre ''chips'' sur la carte mère. Ils sont reconnaissables par leur forme carrée, et surtout par le logo de l'entreprise avec un "Chips" assez reconnaissable.
[[File:Chicony CH-286N-16.jpg|centre|vignette|upright=2|Carte mère avec un ''chipset'' 82C100.]]
==Les coprocesseurs d'entrée-sorties==
Pour finir, nous allons voir un dernier composant qui permet de grandement faciliter la communication avec les entrées-sorties. Il s'agit des '''coprocesseurs d'entrée-sorties''', qui déchargent le processeur principal de tout ce qui a trait aux entrées-sorties. Ils étaient appelés avec des termes très divers : ''Channel I/O'', ''I/O processor'', ''I/O controller'', ''I/O synchronizer'', et autres. Dans ce qui va suivre, nous allons utiliser le terme de '''coprocesseur I/O'''.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Pour simplifier, les coprocesseurs I/O sont des '''contrôleurs DMA programmables'''. Les plus simples sont simplement des contrôleurs DMA émulés avec un processeur, mais la majorité est capables d'enchainer plusieurs transferts DMA à la suite l'un de l'autre. Et certains d'entre eux savaient se débrouiller avec la mémoire virtuelle, avec des capacités de traduction d'adresse virtuelles en adresses physiques.
Ils sont apparus sur les anciens ''mainframes'', notamment sur les ''mainframes'' IBM. A l'époque, communiquer avec des périphérique demandait de faire des transferts DMA, mais les contrôleurs DMA n'existaient pas. Alors les contrôleurs DMA étaient émulés avec des processeurs dédiés, qui s'occupaient des transferts DMA, au jeu d'instruction spécialisé pour gérer des entrées-sorties. Et vu que c'était des processeurs, ils étaient programmables, dans une certaine mesure. C'était l'époque des ''channel I/O'' des vieux ''mainframes''.
Pour les ordinateurs personnels, il a existé un coprocesseur I/O vendu par Intel : l'Intel 8089. Mais l'IBM PC originel ne l'ayant pas utilisé, il n'a pas été repris sur les PC suivants, ce qui l'a fait tomber dans l'oubli. les consoles de jeu utilisent souvent des coprocesseurs I/O. Pour être précis, elles utilisent un processeur normal comme coprocesseur d'entrée-sortie. Un exemple est celui de la console de jeu Nintendo 3DS, qui disposait d'un CPU ARM9, d'un coprocesseur pour les divisions qu'on abordera plus bas, et d'un processeur ARM7. L'ARM 7 était utilisé comme coprocesseur d'I/O, ainsi que pour l'émulation de la console GBA.
Les coprocesseurs I/O exécutent un programme qui agit sur les entrées-sorties, souvent appelé un '''programme I/O'''. Avec un coprocesseur I/O, le CPU initie l'exécution d'un programme I/O sur le coprocesseur I/O. Le coprocesseur IO exécute alors le programme I/O dans son coin, séparément du processeur. Quand le programme IO s'est terminé, le coprocesseur I/O envoie une interruption au CPU pour le prévenir qu'il a terminé. Idem en cas d'erreur lors de la transmission.
===Les interconnexions entre coprocesseur IO, CPU et entrée-sorties===
Un coprocesseur I/O peut être relié au reste de l'ordinateur de plusieurs manières. L'essentiel est que le coprocesseur IO soit relié à la mémoire RAM, pour prendre en charge les transferts DMA. Rappelons en effet qu'un coprocesseur I/O est une sorte de contrôleur DMA sous stéroïde, ce qui requiert qu'il soit relié à la fois aux entrées-sorties et à la mémoire RAM. Il y a donc deux solutions : soit il est connecté à un bus système, soit il est utilisé en tant que répartiteur.
Utiliser un bus système a de nombreux avantages. Le câblage du système est très simple, l'implémentation du DMA sur le coprocesseur I/O est aisée. Par contre, le processeur et le coprocesseur I/O doivent se synchroniser, histoire de ne pas accéder au bus en même temps, en utilisant les mécanismes d'arbitrage du bus.
Utiliser le coprocesseur I/O comme répartiteur fait que celui-ci sert d'intermédiaire entre les entrée-sorties et le reste de l'ordinateur. Il est relié à deux bus : un ''bus processeur'' qui le relie au processeur, et un ''bus IO'' qui le relie aux entrée-sorties. Il reçoit des commandes sur le bus processeur et peut prévenir le processeur quand il a terminé son travail, que ce soit via ''pooling'' ou via des interruptions.
[[File:Connexion d'un coprocesseur IO au reste de l'ordinateur.png|centre|vignette|upright=2|Connexion d'un coprocesseur IO au reste de l'ordinateur]]
Rappelons que le programme I/O doit bien être mis quelque part. Avec un bus système, il est placé dans la RAM système, partagée entre processeur et coprocesseur IO. Pour initialiser le coprocesseur I/O, le processeur a juste besoin de l fournissant un pointeur qui pointe vers le programme IO (l'adresse du programme). Le défaut à cela est que le CPU et le coprocesseur I/O se marchent sur les pieds pour accéder à la mémoire RAM.
Il est possible de résoudre ce problème en dédiant une RAM au coprocesseur, pour le programme IO. Reste à connecter cette RAM dédiée au coprocesseur I/O. Pour cela, il est possible de la placer sur le bus IO, au même titre que n'importe quelle entrée-sortie. C'est la solution la plus économe, mais elle demande d'utiliser le coprocesseur I/O comme un répartiteur, un intermédiaire. Une autre serait de connecter la RAM sur un bus mémoire dédié. Elle a l'avantage de marcher avec un bus système ou une utilisation en répartiteur, mais elle demande que le coprocesseur IO ait des broches dédiées à la mémoire. Dans les deux cas, le programme I/O doit être copié dans cette mémoire séparée, ce qui n'est pas de la tarte.
[[File:Coprocesseur IO avec RAM dédiée sur le bus d'IO.png|centre|vignette|upright=2|Coprocesseur IO avec RAM dédiée sur le bus d'IO]]
Les ''Channel IO'' des anciens ''mainframes'' commandaient un bus IO séparé. Par contre, l'Intel 8089 pouvait fonctionner dans deux modes : soit avec un bus système, soit pour commander un bus IO.
===Le jeu d'instruction du coprocesseur IO===
En général, le programme I/O émule les transferts DMA. Pour cela, le programme IO copie les mots mémoire un par un avec une boucle. Le coprocesseur IO incorpore pour cela les registres d'un contrôleur DMA : des registres pour l'adresse de la source et de la destination, des registres pour des indices de boucle, des compteurs pour le nombre d'octets à copier, etc. Les registres d'adresse sont généralement incrémentés ou décrémentés automatiquement, suivant que le transfert se fasse par adresses croissantes ou décroissantes. Les compteurs de boucles sont décrémentés à chaque itération et mémorisent à tout instant combien de mots mémoire il reste à copier.
Le jeu d'instruction se concentre surtout sur les accès mémoire et les branchements pour les boucles. Pour ce qui est des calculs, il a juste le minimum : addition/soustraction/comparaisons, pas d'instructions de multiplication ou de divisions. Les opérations arithmétiques complexes sont inutiles pour les tâches de gestion des périphériques, elles ne sont donc pas implémentées. Par contre, ils disposent d'instructions de manipulation de bit assez riches et complexes. En effet, les pilotes de périphériques font beaucoup de manipulations de bits, que ce soit pour changer des bits dans un registre de contrôle, extraire les bits pertinents d'un registre d'état, ou bien d'autres manipulations. Pour résumer, leur jeu d’instruction est centré sur : les accès mémoire, les boucles, la manipulation de bit.
Il faut noter que le processeur a souvent plusieurs ports, avec un port par entrée-sortie dans le meilleur des cas. Et chaque port dispose de ses propres registres pointeurs/computer de boucle. Par exemple, l'Intel 8089 avait deux ports dédiés aux entrée-sorties, et deux bancs de registres dédiés. Rien de surprenant, il y a l'équivalent sur n'importe quel contrôleur DMA. Chaque canal DMA a ses propres registres.
Les coprocesseurs I/O peuvent faire communiquer des composants ayant des largeurs de bus différentes. Par exemple, il est possible de faire communiquer une entrés-sortie 8 bits avec une autre de 16 bits, ou de faire des transferts entre une RAM 32 bits et une entrée-sortie de 16 bits. Pour cela, le coprocesseur I/O fait automatiquement la conversion. Par exemple, pour un transfert d'un composant 16 bits vers un autre de 8 bits, il lit 16 bits depuis la source et découpe le doublet en deux octets, qu'il envoie un par un. Inversement, pour un transfert d'un composant 8 bits vers un composant 16 bits, il lit deux octets à la suite et les combine en un seul doublet, envoyé en une fois.
===L'Intel 8089===
[[File:Intel 8089.svg|vignette|Intel 8089]]
L'Intel 8089 est un coprocesseur Intel, prévu pour fonctionner en tandem avec un CPU 8086 d'Intel. Il contient deux canaux DMA programmables, chacun ayant ses propres registres, ses propres interruptions, etc. Par contre, le reste du processeur n'est présent qu'en un seul exemplaire. Ce qui veut dire que ce n'est pas un processeur double cœur, il n'y a qu'une seule unité de contrôle, et une seule unité de calcul.
Niveau instruction, on retrouve les instructions classiques, avec quelques ajouts dédiés aux transferts DMA. Les calculs se limitent à l'addition, l'incrémentation, la décrémentation, et les ET/OU/NON bit à bit. Il y a aussi deux instructions pour mettre un bit à 0 ou 1, ainsi que des branchements pour tester la valeur d'un bit dans un registre ou une adresse. Mais les instructions intéressantes sont :
* les instructions de copie mémoire-mémoire ;
* une instruction pour configurer un transfert DMA ;
* une instruction pour démarrer un transfert DMA à la prochaine instruction ;
* une instruction pour terminer l'exécution du programme I/O ;
* une instruction pour mettre la sortie d’interruption à 1.
Le 8089 contient des registres de 20/21 bits et des registres de 16 bits. Les registres de 20/21 bits sont destinés à recevoir des adresses, alors que les registres de 16 bits sont des registres de données. Si je dis 20/21, c'est en raison de l'adressage employé par le 8086, qui utilisait un espace d'adressage séparé pour la mémoire et les entrée-sorties. Les registres du 8089 mémorisent des adresses de 20 bits, et ajoutent un bit IO pour faire la différence entre les deux espaces d'adressage.
Les registres d'adresse sont au nombre de quatre et sont nommés GA, GB, GC et TP. Les registres de 16 bits sont eux aussi au nombre de quatre et sont nommés BC, IX, MC et CC. Le registre TP est le ''program counter'' associé à un canal DMA. Les deux canaux DMA exécutent chacun leur propre programme, ce qui fait qu'ils ont chacun leur propre ''program counter''. Mais pour le reste, les registres ne font pas la même chose selon qu'un transfert DMA est en cours ou non.
Lors d'un transfert DMA, les registres sont utilisés comme suit :
* Le registre GA est utilisé pour adresser la source, le registre GB est utilisé pour adresser la destination.
* Le registre GC est facultatif : il pointe vers une table de 256 octets, utilisée pour faire de la traduction d'adresse.
* Le registre BC compte le nombre d'octets qu'il reste à transférer. Le registre MC mémorise un masque, qui est utilisé pour appliquer une opération de masquage avant d'écrire l'octet dans la destination.
* Le registre CC est utilisé pour configurer certains transferts DMA, notamment quand plusieurs transferts DMA sont enchainés automatiquement par le 8089.
* Les autres registres ne sont pas utilisés.
En dehors d'un transfert DMA, les registres sont utilisés pour stocker des données, à l'exception de TP et de CC.
* Les trois registres d'adresse GA, GB et GC peuvent être utilisés comme registre de donnée, entre deux transferts DMA. Mais ils sont alors utilisés comme des registres de 16 bits, les 4 bits de poids fort étant remplis avec le bit de signe du nombre qu'ils contiennent.
* Le registre BC n'a pas de prédisposition en dehors des transferts DMA.
* IX est prédisposé à servir de registre d'indice, si le mode d'adressage le demande.
* MC est prédisposé à gérer un masque, qui est utilisé avec les instructions JMCE and JMCNE.
Initialiser le 8089 demande de lui fournir des informations, regroupées dans deux blocs de mémoire : un ''Channel Control Block'' et un ''Command Parameter Block''. Les deux sont placés en mémoire RAM, le 8089 y accède en lisant dans la mémoire RAM, et le processeur doit fournir l'adresse de ces deux blocs de configuration au 8089 quand il l'initialise. Je ne vais pas donner une explication complète de ces deux blocs, mais je vais quand même donner quelques détails.
Le ''Channel Control Block'' contient, pour chaque canal DMA, un octet de configuration. L'octet de configuration contient plusieurs bits de configuration. En premier lieu, on trouve trois bits qui permettent de démarrer, terminer, suspendre ou reprendre l'exécution du programme I/O. En second lieu, on trouve deux bits pour activer/désactiver les interruptions sur chaque canal DMA. Enfin, on a un bit de priorité qui permet de donner la priorité à un canal DMA sur l'autre. Si un canal DMA a ce bit à 1 et l'autre a un bit à 0, celui qui a le bit à 1 a la priorité.
Le ''Command Parameter Block'' contient l'adresse du programme I/O, en mémoire RAM. Elle est en réalité composée de deux adresses, parce que le 8089 et le 8086 utilisent la segmentation. Il contient aussi un ''program status word'', qui remplace le registre d'état. En effet, le 8089 n'a pas vraiment de registre d'état. A la place, il utilise un octet en RAM, qui n'est autre que le ''program status word''. Il met à jour le ''program status word'' en mémoire RAM dès que nécessaire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'abstraction mémoire et la mémoire virtuelle
| prevText=L'abstraction mémoire et la mémoire virtuelle
| next=L'adressage des périphériques
| nextText=L'adressage des périphériques
}}
</noinclude>
12zqyhhuq3sb0c3fcba17w9uywkdgmy
La grammaire fondamentale de l'ido
0
83682
763038
762148
2026-04-06T09:30:10Z
~2026-21142-78
123434
763038
wikitext
text/x-wiki
[[File:Standardised Ido Flag.svg|thumb|Le drapeau de l'ido.]]
L''''ido''' est une langue construite créée au début du XXe siècle. Elle a été conçue conjointement par des érudits — linguistes, logiciens et scientifiques — issus de la ''Délégation pour l'adoption d'une langue auxiliaire internationale'', en s'appuyant sur les bases de l'espéranto, avec l'ambition de devenir une langue mieux adaptée à la communication internationale. Par rapport à son prédécesseur, l'espéranto, l'ido a introduit de nombreuses réformes, telles que la neutralisation du genre des noms, la simplification de la prononciation, une orthographe plus compatible et l'élimination des redondances grammaticales, afin d'en faire une langue plus scientifique et naturelle.
Une grande partie du vocabulaire de l'ido provient des principales langues indo-européennes. Si le lecteur possède des bases en français, en anglais, en espagnol, en allemand, en italien ou en latin, la compréhension des mots et de la grammaire lui sera plus familière et aisée. Pour les termes inconnus, le lecteur peut consulter le '''Wiktionnaire''' ou le dictionnaire '''Glosbe'''.
Ce manuel a pour objectif de permettre aux débutants de découvrir et d'apprendre rapidement la grammaire rigoureuse et élégante de l'ido. Les traducteurs sont invités à localiser cet ouvrage dans d'autres langues, et toute correction ou enrichissement du contenu est la bienvenue. Enfin, ''bona chanco'' (bonne chance) !
== Sommaire ==
# [[/Écriture et prononciation | Écriture et prononciation]]
# [[/Composition du lexique | Composition du lexique]]
# [[/Mots lexicaux | Mots lexicaux]]
# [[/Mots grammaticaux | Mots grammaticaux]]
# [[/Structure de la phrase | Structure de la phrase]]
# [[/Formation des mots | Formation des mots]]
# [[/Affixes | Affixes]]
# [[/Suggestions de pratique | Suggestions de pratique]]
== Notice ==
<blockquote>
''La bazal gramatiko di l'Ido, skribita da Francucelo en 2026.''
</blockquote>
[[Catégorie:Langues construites]]
41fy0y4moppzhg4hdnat4wdr60g8t9z
Guide des mots rares à adopter
0
83748
762987
762940
2026-04-05T16:50:18Z
ROSEMARSH HOOD
122846
Mise à jour de la page
762987
wikitext
text/x-wiki
{{Nouveau livre}}{{NavDébut|book=Guide des mots rares à adopter|page=Introduction|pageText=Démarrer}}{{Page de garde
| 2 = VisualEditor - Icon - Open-book-2.svg
| description = <big>'''{{Centrer|Découvrez divers mots rares et curiosités lexicales à adopter pour enrichir votre vocabulaire et donner du style à votre langage quotidien}}'''</big><br/>
'''''Guide des mots rares à adopter pour enrichir son vocabulaire''''' est un wikilivre incontournable pour les passionnés de langue française, les écrivains et les curieux des mots. Il propose une sélection soignée de termes rares, élégants ou méconnus, accompagnés de leurs définitions, origines et exemples d’usage concrets. S’appuyant notamment sur des ressources linguistiques comme le [[W:Lexique informatisé des mots insolites à étymologie latine|''LiMiEL'' (Lexique informatisé des mots insolites à étymologie latine)]], ce guide met en lumière la richesse du lexique français tout en offrant des clés pratiques pour intégrer ces mots dans son expression écrite et orale. Idéal pour enrichir son vocabulaire, affiner son style et redécouvrir la beauté des mots.
| avancement = Avancé
}}
== Source centrale du livre ==
Ce [[wikt:livre|livre]] s’appuie en grande partie sur les [[wikt:données_ouvertes|données]] du ''[[w:Lexique_informatisé_des_mots_insolites_à_étymologie_latine|Lexique informatisé des mots insolites à étymologie latine]]'', qui constitue une ressource de référence pour l’[[wikt:identification|identification]] et l’[[wikt:étude|étude]] de [[w:Mot_rare|mots rares]] en [[français]]. Les contenus issus de cette base sont utilisés conformément à leur [[Licences libres|licence]] '''[[Licences libres#Pour les données|ODC-By (Open Data Commons Attribution)]]''', qui autorise la [[wikt:réutilisation|réutilisation]], la [[wikt:modification|modification]] et la [[w:Données_ouvertes#Open_Database_Commons|diffusion des données à condition d’en mentionner la source.]] Cette approche garantit à la fois la [[w:Lexique|richesse lexicale]] du [[wikt:Modèle:R:LiMiEL|guide]] et le [[w:Licence_libre|respect]] des principes de [[wikt:données_ouvertes|partage ouvert]] des [[wikt:connaissance|connaissances]]<ref>https://limiel.omeka.net/licence_odc-by</ref>.
Un [[wikt:Wikilivres|wikilivre]] à été rédigé pour apprendre à [[wikt:consulter|naviguer]] dans ce [[wikt:dictionnaire|dictionnaire]] [[wikt:numérique|numérique]] : [[Utiliser le LiMiEL de manière productive]]
== Qu'est-ce qu'un [[w:Mot_rare|mot rare]] ? ==
Un '''[[w:Mot_rare|mot rare]]''' est une [[w:Lexème|unité lexicale]] '''peu utilisée''' dans la [[w:Français|langue française]]. Selon le ''TLFi'' (''[[w:Trésor_de_la_langue_française_informatisé|Trésor de la langue française informatisé]]''), ces mots apparaissent rarement dans les [[w:Corpus|corpus littéraires]] et dans la langue quotidienne. Les dictionnaires les signalent souvent comme '''[[wikt:ancien|anciens,]] [[wikt:littéraire|littéraires]] ou [[wikt:ancien|rares]]'''[[wikt:ancien|,]] ce qui les distingue clairement des mots courants fréquemment employés dans le français parlé et écrit<ref name=":0">Josette Rey-Debove, ''Étude linguistique et sémiotique des dictionnaires français contemporains'', Mouton, La Haye & Paris, 1971, p. 81.</ref><ref>https://usito.usherbrooke.ca/lexies/mots/rare</ref>.
== Sommaire ==
* [[Guide des mots rares à adopter/Introduction|Introduction]]
''<small>([[wikt:cliquer|cliquer]] sur le [https://fr.wikipedia.org/w/index.php?title=Mot_rare&veaction=edit mot rare] pour lire sa fiche complète)</small>''
* [[Guide des mots rares à adopter/crépat, crépate|'''crépat, crépate''']] : qui produit un effet sonore '''bruyant mais envoûtant''', capable de captiver l’attention par son intensité harmonieuse ou rythmique ;
* [[Guide des mots rares à adopter/clangart, clangarte|'''clangart, clangarte''']] : qui produit un effet sonore '''soudainement assourdissant''', souvent perçu comme agressif, brutal ou désagréable ;
* [[Guide des mots rares à adopter/caullée|'''caullée''']] : ensemble de '''cavités ou de précipices contigus''' ;
* [[Guide des mots rares à adopter/aconcordesque|'''aconcordesque''']] : qui, dans une relation ou une situation affective, '''trahit une concordance amoureuse''', rompt une harmonie sentimentale ou va à l’encontre des élans du cœur ;
* [[Guide des mots rares à adopter/aéruminal, aéruminale|'''aéruminal, aéruminale''']] : qui est '''accablé de peine, de misères ou de tristesse profonde''' ;
* [[Guide des mots rares à adopter/audulard, audularde|'''audulard, audularde''']] : qui '''se laisse facilement flatter''', qui est particulièrement '''réceptif à l’adulation''' ou aux compliments intéressés ;
* [[Guide des mots rares à adopter/auricolore|'''auricolore''']] : qui possède '''une couleur d’or ou une teinte dorée''', brillante et éclatante ;
* [[Guide des mots rares à adopter/blatérate|'''blatérate''']] : qui '''ne mentionne rien de concret''', mais qui le donne à croire par son ton, son style ou son vocabulaire soigné ;
* [[Guide des mots rares à adopter/blatération|'''blatération''']] : '''bavardage frivole''' '''et superficiel''', suite de paroles vaines ou légères, souvent répétitives et sans réel contenu ;
* [[Guide des mots rares à adopter/burgenatif, burgenative|'''burgenatif, burgenative''']] : personne née d'une '''famille riche ou bourgeoise''' ''';'''
* [[Guide des mots rares à adopter/emphygomphe|'''emphygomphe''']] : '''proclamation lyrique excessivement emphatique''', marquée par une exagération du ton et une importance démesurée accordée à des propos souvent creux ;
* [[Guide des mots rares à adopter/clausible|'''clausible''']]
* [[Guide des mots rares à adopter/clausibilité|'''clausibilité''']]
* [[Guide des mots rares à adopter/brèviloquent, brèviloquente|'''brèviloquent, brèviloquente''']]
*''(autres à venir)''
== Notes ==
<references />
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
23lg3qj9r9tsznxr2ryf64gckbcutfd
762988
762987
2026-04-05T16:50:56Z
ROSEMARSH HOOD
122846
/* Sommaire */
762988
wikitext
text/x-wiki
{{Nouveau livre}}{{NavDébut|book=Guide des mots rares à adopter|page=Introduction|pageText=Démarrer}}{{Page de garde
| 2 = VisualEditor - Icon - Open-book-2.svg
| description = <big>'''{{Centrer|Découvrez divers mots rares et curiosités lexicales à adopter pour enrichir votre vocabulaire et donner du style à votre langage quotidien}}'''</big><br/>
'''''Guide des mots rares à adopter pour enrichir son vocabulaire''''' est un wikilivre incontournable pour les passionnés de langue française, les écrivains et les curieux des mots. Il propose une sélection soignée de termes rares, élégants ou méconnus, accompagnés de leurs définitions, origines et exemples d’usage concrets. S’appuyant notamment sur des ressources linguistiques comme le [[W:Lexique informatisé des mots insolites à étymologie latine|''LiMiEL'' (Lexique informatisé des mots insolites à étymologie latine)]], ce guide met en lumière la richesse du lexique français tout en offrant des clés pratiques pour intégrer ces mots dans son expression écrite et orale. Idéal pour enrichir son vocabulaire, affiner son style et redécouvrir la beauté des mots.
| avancement = Avancé
}}
== Source centrale du livre ==
Ce [[wikt:livre|livre]] s’appuie en grande partie sur les [[wikt:données_ouvertes|données]] du ''[[w:Lexique_informatisé_des_mots_insolites_à_étymologie_latine|Lexique informatisé des mots insolites à étymologie latine]]'', qui constitue une ressource de référence pour l’[[wikt:identification|identification]] et l’[[wikt:étude|étude]] de [[w:Mot_rare|mots rares]] en [[français]]. Les contenus issus de cette base sont utilisés conformément à leur [[Licences libres|licence]] '''[[Licences libres#Pour les données|ODC-By (Open Data Commons Attribution)]]''', qui autorise la [[wikt:réutilisation|réutilisation]], la [[wikt:modification|modification]] et la [[w:Données_ouvertes#Open_Database_Commons|diffusion des données à condition d’en mentionner la source.]] Cette approche garantit à la fois la [[w:Lexique|richesse lexicale]] du [[wikt:Modèle:R:LiMiEL|guide]] et le [[w:Licence_libre|respect]] des principes de [[wikt:données_ouvertes|partage ouvert]] des [[wikt:connaissance|connaissances]]<ref>https://limiel.omeka.net/licence_odc-by</ref>.
Un [[wikt:Wikilivres|wikilivre]] à été rédigé pour apprendre à [[wikt:consulter|naviguer]] dans ce [[wikt:dictionnaire|dictionnaire]] [[wikt:numérique|numérique]] : [[Utiliser le LiMiEL de manière productive]]
== Qu'est-ce qu'un [[w:Mot_rare|mot rare]] ? ==
Un '''[[w:Mot_rare|mot rare]]''' est une [[w:Lexème|unité lexicale]] '''peu utilisée''' dans la [[w:Français|langue française]]. Selon le ''TLFi'' (''[[w:Trésor_de_la_langue_française_informatisé|Trésor de la langue française informatisé]]''), ces mots apparaissent rarement dans les [[w:Corpus|corpus littéraires]] et dans la langue quotidienne. Les dictionnaires les signalent souvent comme '''[[wikt:ancien|anciens,]] [[wikt:littéraire|littéraires]] ou [[wikt:ancien|rares]]'''[[wikt:ancien|,]] ce qui les distingue clairement des mots courants fréquemment employés dans le français parlé et écrit<ref name=":0">Josette Rey-Debove, ''Étude linguistique et sémiotique des dictionnaires français contemporains'', Mouton, La Haye & Paris, 1971, p. 81.</ref><ref>https://usito.usherbrooke.ca/lexies/mots/rare</ref>.
== Sommaire ==
* [[Guide des mots rares à adopter/Introduction|Introduction]]
''<small>([[wikt:cliquer|cliquer]] sur le [[w:Mot_rare|mot rare]] pour lire sa fiche complète)</small>''
* [[Guide des mots rares à adopter/crépat, crépate|'''crépat, crépate''']] : qui produit un effet sonore '''bruyant mais envoûtant''', capable de captiver l’attention par son intensité harmonieuse ou rythmique ;
* [[Guide des mots rares à adopter/clangart, clangarte|'''clangart, clangarte''']] : qui produit un effet sonore '''soudainement assourdissant''', souvent perçu comme agressif, brutal ou désagréable ;
* [[Guide des mots rares à adopter/caullée|'''caullée''']] : ensemble de '''cavités ou de précipices contigus''' ;
* [[Guide des mots rares à adopter/aconcordesque|'''aconcordesque''']] : qui, dans une relation ou une situation affective, '''trahit une concordance amoureuse''', rompt une harmonie sentimentale ou va à l’encontre des élans du cœur ;
* [[Guide des mots rares à adopter/aéruminal, aéruminale|'''aéruminal, aéruminale''']] : qui est '''accablé de peine, de misères ou de tristesse profonde''' ;
* [[Guide des mots rares à adopter/audulard, audularde|'''audulard, audularde''']] : qui '''se laisse facilement flatter''', qui est particulièrement '''réceptif à l’adulation''' ou aux compliments intéressés ;
* [[Guide des mots rares à adopter/auricolore|'''auricolore''']] : qui possède '''une couleur d’or ou une teinte dorée''', brillante et éclatante ;
* [[Guide des mots rares à adopter/blatérate|'''blatérate''']] : qui '''ne mentionne rien de concret''', mais qui le donne à croire par son ton, son style ou son vocabulaire soigné ;
* [[Guide des mots rares à adopter/blatération|'''blatération''']] : '''bavardage frivole''' '''et superficiel''', suite de paroles vaines ou légères, souvent répétitives et sans réel contenu ;
* [[Guide des mots rares à adopter/burgenatif, burgenative|'''burgenatif, burgenative''']] : personne née d'une '''famille riche ou bourgeoise''' ''';'''
* [[Guide des mots rares à adopter/emphygomphe|'''emphygomphe''']] : '''proclamation lyrique excessivement emphatique''', marquée par une exagération du ton et une importance démesurée accordée à des propos souvent creux ;
* [[Guide des mots rares à adopter/clausible|'''clausible''']]
* [[Guide des mots rares à adopter/clausibilité|'''clausibilité''']]
* [[Guide des mots rares à adopter/brèviloquent, brèviloquente|'''brèviloquent, brèviloquente''']]
*''(autres à venir)''
== Notes ==
<references />
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
mrfrxtxbw2e1wvtt21c4ql09jmz9jdt
763009
762988
2026-04-05T20:34:47Z
ROSEMARSH HOOD
122846
Ajout de ressources externes
763009
wikitext
text/x-wiki
{{Nouveau livre}}{{NavDébut|book=Guide des mots rares à adopter|page=Introduction|pageText=Démarrer}}{{Page de garde
| 2 = VisualEditor - Icon - Open-book-2.svg
| description = <big>'''{{Centrer|Découvrez divers mots rares et curiosités lexicales à adopter pour enrichir votre vocabulaire et donner du style à votre langage quotidien}}'''</big><br/>
'''''Guide des mots rares à adopter pour enrichir son vocabulaire''''' est un wikilivre incontournable pour les passionnés de langue française, les écrivains et les curieux des mots. Il propose une sélection soignée de termes rares, élégants ou méconnus, accompagnés de leurs définitions, origines et exemples d’usage concrets. S’appuyant notamment sur des ressources linguistiques comme le [[W:Lexique informatisé des mots insolites à étymologie latine|''LiMiEL'' (Lexique informatisé des mots insolites à étymologie latine)]], ce guide met en lumière la richesse du lexique français tout en offrant des clés pratiques pour intégrer ces mots dans son expression écrite et orale. Idéal pour enrichir son vocabulaire, affiner son style et redécouvrir la beauté des mots.
| avancement = Avancé
}}
== Source centrale du livre ==
Ce [[wikt:livre|livre]] s’appuie en grande partie sur les [[wikt:données_ouvertes|données]] du ''[[w:Lexique_informatisé_des_mots_insolites_à_étymologie_latine|Lexique informatisé des mots insolites à étymologie latine]]'', qui constitue une ressource de référence pour l’[[wikt:identification|identification]] et l’[[wikt:étude|étude]] de [[w:Mot_rare|mots rares]] en [[français]]. Les contenus issus de cette base sont utilisés conformément à leur [[Licences libres|licence]] '''[[Licences libres#Pour les données|ODC-By (Open Data Commons Attribution)]]''', qui autorise la [[wikt:réutilisation|réutilisation]], la [[wikt:modification|modification]] et la [[w:Données_ouvertes#Open_Database_Commons|diffusion des données à condition d’en mentionner la source.]] Cette approche garantit à la fois la [[w:Lexique|richesse lexicale]] du [[wikt:Modèle:R:LiMiEL|guide]] et le [[w:Licence_libre|respect]] des principes de [[wikt:données_ouvertes|partage ouvert]] des [[wikt:connaissance|connaissances]]<ref>https://limiel.omeka.net/licence_odc-by</ref>.
Un [[wikt:Wikilivres|wikilivre]] à été rédigé pour apprendre à [[wikt:consulter|naviguer]] dans ce [[wikt:dictionnaire|dictionnaire]] [[wikt:numérique|numérique]] : [[Utiliser le LiMiEL de manière productive]]
== Qu'est-ce qu'un [[w:Mot_rare|mot rare]] ? ==
Un '''[[w:Mot_rare|mot rare]]''' est une [[w:Lexème|unité lexicale]] '''peu utilisée''' dans la [[w:Français|langue française]]. Selon le ''TLFi'' (''[[w:Trésor_de_la_langue_française_informatisé|Trésor de la langue française informatisé]]''), ces mots apparaissent rarement dans les [[w:Corpus|corpus littéraires]] et dans la langue quotidienne. Les dictionnaires les signalent souvent comme '''[[wikt:ancien|anciens,]] [[wikt:littéraire|littéraires]] ou [[wikt:ancien|rares]]'''[[wikt:ancien|,]] ce qui les distingue clairement des mots courants fréquemment employés dans le français parlé et écrit<ref name=":0">Josette Rey-Debove, ''Étude linguistique et sémiotique des dictionnaires français contemporains'', Mouton, La Haye & Paris, 1971, p. 81.</ref><ref>https://usito.usherbrooke.ca/lexies/mots/rare</ref>.
== Sommaire ==
* [[Guide des mots rares à adopter/Introduction|Introduction]]
''<small>([[wikt:cliquer|cliquer]] sur le [[w:Mot_rare|mot rare]] pour lire sa fiche complète)</small>''
* [[Guide des mots rares à adopter/crépat, crépate|'''crépat, crépate''']] : qui produit un effet sonore '''bruyant mais envoûtant''', capable de captiver l’attention par son intensité harmonieuse ou rythmique ;
* [[Guide des mots rares à adopter/clangart, clangarte|'''clangart, clangarte''']] : qui produit un effet sonore '''soudainement assourdissant''', souvent perçu comme agressif, brutal ou désagréable ;
* [[Guide des mots rares à adopter/caullée|'''caullée''']] : ensemble de '''cavités ou de précipices contigus''' ;
* [[Guide des mots rares à adopter/aconcordesque|'''aconcordesque''']] : qui, dans une relation ou une situation affective, '''trahit une concordance amoureuse''', rompt une harmonie sentimentale ou va à l’encontre des élans du cœur ;
* [[Guide des mots rares à adopter/aéruminal, aéruminale|'''aéruminal, aéruminale''']] : qui est '''accablé de peine, de misères ou de tristesse profonde''' ;
* [[Guide des mots rares à adopter/audulard, audularde|'''audulard, audularde''']] : qui '''se laisse facilement flatter''', qui est particulièrement '''réceptif à l’adulation''' ou aux compliments intéressés ;
* [[Guide des mots rares à adopter/auricolore|'''auricolore''']] : qui possède '''une couleur d’or ou une teinte dorée''', brillante et éclatante ;
* [[Guide des mots rares à adopter/blatérate|'''blatérate''']] : qui '''ne mentionne rien de concret''', mais qui le donne à croire par son ton, son style ou son vocabulaire soigné ;
* [[Guide des mots rares à adopter/blatération|'''blatération''']] : '''bavardage frivole''' '''et superficiel''', suite de paroles vaines ou légères, souvent répétitives et sans réel contenu ;
* [[Guide des mots rares à adopter/burgenatif, burgenative|'''burgenatif, burgenative''']] : personne née d'une '''famille riche ou bourgeoise''' ;
* [[Guide des mots rares à adopter/emphygomphe|'''emphygomphe''']] : '''proclamation lyrique excessivement emphatique''', marquée par une exagération du ton et une importance démesurée accordée à des propos souvent creux ;
* [[Guide des mots rares à adopter/clausible|'''clausible''']] : '''qui peut se refermer ou se sceller sur soi-même sans perte d’intégrité''' ;
* [[Guide des mots rares à adopter/clausibilité|'''clausibilité''']] : capacité d’un objet, d’un système ou d’un ensemble '''à se refermer sur lui-même ou à se sceller sans perte d’intégrité''' ;
* [[Guide des mots rares à adopter/brèviloquent, brèviloquente|'''brèviloquent, brèviloquente''']] : qui se caractérise par une expression sobre, concise et mesurée, '''privilégiant la brièveté dans la parole ou l’écriture'''.
*''<small>(autres à venir)</small>''
== Pour aller plus loin ; autres ressources consacrées aux curiosités lexicales ==
=== Ouvrages ===
* Le Drouviot, ''dictionnaire des mots rares ou exceptionnels de la langue française'' [http://drouviot.net/dictionnaire → consulter cet ouvrage]
* LiMiEL, ''Lexique informatisé des mots insolites à étymologie latine'', depuis 2025 [https://limiel.omeka.net → consulter cet ouvrage]
=== Articles ===
* « Plus de 100 mots rares pour enrichir votre vocabulaire », par Adrian, dans ''La Culture Générale'', 28 janvier 2019 [https://www.laculturegenerale.com/ameliorer-vocabulaire-enrichir/ → consulter cet article]
* « 10 mots insolites de la langue française », dans ''Éditions Maison des Langues'', 24 octobre 2024 [https://www.emdl.fr/lettres/dernieres-actualites/10-mots-insolites-de-la-langue-francaise → consulter cet article]
* « Top 20 des mots rares que tout le monde devrait connaître », par Jeannou Pagure, dans ''Topito'', 31 octobre 2022 [https://www.topito.com/top-mots-rares-devrait-connaitre → consulter cet article]
* « Cinq mots rares (et précieux) que nous ferions bien d’employer », par Claire Conruyt, dans ''Le Figaro'', 5 janvier 2020 [https://www.lefigaro.fr/langue-francaise/expressions-francaises/cinq-mots-rares-et-precieux-que-nous-ferions-bien-d-employer-20200105 → consulter cet article]
== Notes et références ==
<references />
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
2w0ues5iyyr3pysjgluhn1hc0yqo3h8
763010
763009
2026-04-05T21:13:39Z
ROSEMARSH HOOD
122846
Déplacement bannière
763010
wikitext
text/x-wiki
{{NavDébut|book=Guide des mots rares à adopter|page=Introduction|pageText=Démarrer}}{{Page de garde
| 2 = VisualEditor - Icon - Open-book-2.svg
| description = <big>'''{{Centrer|Découvrez divers mots rares et curiosités lexicales à adopter pour enrichir votre vocabulaire et donner du style à votre langage quotidien}}'''</big><br/>
'''''Guide des mots rares à adopter pour enrichir son vocabulaire''''' est un wikilivre incontournable pour les passionnés de langue française, les écrivains et les curieux des mots. Il propose une sélection soignée de termes rares, élégants ou méconnus, accompagnés de leurs définitions, origines et exemples d’usage concrets. S’appuyant notamment sur des ressources linguistiques comme le [[W:Lexique informatisé des mots insolites à étymologie latine|''LiMiEL'' (Lexique informatisé des mots insolites à étymologie latine)]], ce guide met en lumière la richesse du lexique français tout en offrant des clés pratiques pour intégrer ces mots dans son expression écrite et orale. Idéal pour enrichir son vocabulaire, affiner son style et redécouvrir la beauté des mots.
| avancement = Avancé
}}
== Source centrale du livre ==
Ce [[wikt:livre|livre]] s’appuie en grande partie sur les [[wikt:données_ouvertes|données]] du ''[[w:Lexique_informatisé_des_mots_insolites_à_étymologie_latine|Lexique informatisé des mots insolites à étymologie latine]]'', qui constitue une ressource de référence pour l’[[wikt:identification|identification]] et l’[[wikt:étude|étude]] de [[w:Mot_rare|mots rares]] en [[français]]. Les contenus issus de cette base sont utilisés conformément à leur [[Licences libres|licence]] '''[[Licences libres#Pour les données|ODC-By (Open Data Commons Attribution)]]''', qui autorise la [[wikt:réutilisation|réutilisation]], la [[wikt:modification|modification]] et la [[w:Données_ouvertes#Open_Database_Commons|diffusion des données à condition d’en mentionner la source.]] Cette approche garantit à la fois la [[w:Lexique|richesse lexicale]] du [[wikt:Modèle:R:LiMiEL|guide]] et le [[w:Licence_libre|respect]] des principes de [[wikt:données_ouvertes|partage ouvert]] des [[wikt:connaissance|connaissances]]<ref>https://limiel.omeka.net/licence_odc-by</ref>.
Un [[wikt:Wikilivres|wikilivre]] à été rédigé pour apprendre à [[wikt:consulter|naviguer]] dans ce [[wikt:dictionnaire|dictionnaire]] [[wikt:numérique|numérique]] : [[Utiliser le LiMiEL de manière productive]]
== Qu'est-ce qu'un [[w:Mot_rare|mot rare]] ? ==
Un '''[[w:Mot_rare|mot rare]]''' est une [[w:Lexème|unité lexicale]] '''peu utilisée''' dans la [[w:Français|langue française]]. Selon le ''TLFi'' (''[[w:Trésor_de_la_langue_française_informatisé|Trésor de la langue française informatisé]]''), ces mots apparaissent rarement dans les [[w:Corpus|corpus littéraires]] et dans la langue quotidienne. Les dictionnaires les signalent souvent comme '''[[wikt:ancien|anciens,]] [[wikt:littéraire|littéraires]] ou [[wikt:ancien|rares]]'''[[wikt:ancien|,]] ce qui les distingue clairement des mots courants fréquemment employés dans le français parlé et écrit<ref name=":0">Josette Rey-Debove, ''Étude linguistique et sémiotique des dictionnaires français contemporains'', Mouton, La Haye & Paris, 1971, p. 81.</ref><ref>https://usito.usherbrooke.ca/lexies/mots/rare</ref>.
== Sommaire ==
* [[Guide des mots rares à adopter/Introduction|Introduction]]
''<small>([[wikt:cliquer|cliquer]] sur le [[w:Mot_rare|mot rare]] pour lire sa fiche complète)</small>''
* [[Guide des mots rares à adopter/crépat, crépate|'''crépat, crépate''']] : qui produit un effet sonore '''bruyant mais envoûtant''', capable de captiver l’attention par son intensité harmonieuse ou rythmique ;
* [[Guide des mots rares à adopter/clangart, clangarte|'''clangart, clangarte''']] : qui produit un effet sonore '''soudainement assourdissant''', souvent perçu comme agressif, brutal ou désagréable ;
* [[Guide des mots rares à adopter/caullée|'''caullée''']] : ensemble de '''cavités ou de précipices contigus''' ;
* [[Guide des mots rares à adopter/aconcordesque|'''aconcordesque''']] : qui, dans une relation ou une situation affective, '''trahit une concordance amoureuse''', rompt une harmonie sentimentale ou va à l’encontre des élans du cœur ;
* [[Guide des mots rares à adopter/aéruminal, aéruminale|'''aéruminal, aéruminale''']] : qui est '''accablé de peine, de misères ou de tristesse profonde''' ;
* [[Guide des mots rares à adopter/audulard, audularde|'''audulard, audularde''']] : qui '''se laisse facilement flatter''', qui est particulièrement '''réceptif à l’adulation''' ou aux compliments intéressés ;
* [[Guide des mots rares à adopter/auricolore|'''auricolore''']] : qui possède '''une couleur d’or ou une teinte dorée''', brillante et éclatante ;
* [[Guide des mots rares à adopter/blatérate|'''blatérate''']] : qui '''ne mentionne rien de concret''', mais qui le donne à croire par son ton, son style ou son vocabulaire soigné ;
* [[Guide des mots rares à adopter/blatération|'''blatération''']] : '''bavardage frivole''' '''et superficiel''', suite de paroles vaines ou légères, souvent répétitives et sans réel contenu ;
* [[Guide des mots rares à adopter/burgenatif, burgenative|'''burgenatif, burgenative''']] : personne née d'une '''famille riche ou bourgeoise''' ;
* [[Guide des mots rares à adopter/emphygomphe|'''emphygomphe''']] : '''proclamation lyrique excessivement emphatique''', marquée par une exagération du ton et une importance démesurée accordée à des propos souvent creux ;
* [[Guide des mots rares à adopter/clausible|'''clausible''']] : '''qui peut se refermer ou se sceller sur soi-même sans perte d’intégrité''' ;
* [[Guide des mots rares à adopter/clausibilité|'''clausibilité''']] : capacité d’un objet, d’un système ou d’un ensemble '''à se refermer sur lui-même ou à se sceller sans perte d’intégrité''' ;
* [[Guide des mots rares à adopter/brèviloquent, brèviloquente|'''brèviloquent, brèviloquente''']] : qui se caractérise par une expression sobre, concise et mesurée, '''privilégiant la brièveté dans la parole ou l’écriture'''.
*''<small>(autres à venir)</small>''
== Pour aller plus loin ; autres ressources consacrées aux curiosités lexicales ==
=== Ouvrages ===
* Le Drouviot, ''dictionnaire des mots rares ou exceptionnels de la langue française'' [http://drouviot.net/dictionnaire → consulter cet ouvrage]
* LiMiEL, ''Lexique informatisé des mots insolites à étymologie latine'', depuis 2025 [https://limiel.omeka.net → consulter cet ouvrage]
=== Articles ===
* « Plus de 100 mots rares pour enrichir votre vocabulaire », par Adrian, dans ''La Culture Générale'', 28 janvier 2019 [https://www.laculturegenerale.com/ameliorer-vocabulaire-enrichir/ → consulter cet article]
* « 10 mots insolites de la langue française », dans ''Éditions Maison des Langues'', 24 octobre 2024 [https://www.emdl.fr/lettres/dernieres-actualites/10-mots-insolites-de-la-langue-francaise → consulter cet article]
* « Top 20 des mots rares que tout le monde devrait connaître », par Jeannou Pagure, dans ''Topito'', 31 octobre 2022 [https://www.topito.com/top-mots-rares-devrait-connaitre → consulter cet article]
* « Cinq mots rares (et précieux) que nous ferions bien d’employer », par Claire Conruyt, dans ''Le Figaro'', 5 janvier 2020 [https://www.lefigaro.fr/langue-francaise/expressions-francaises/cinq-mots-rares-et-precieux-que-nous-ferions-bien-d-employer-20200105 → consulter cet article]
== Notes et références ==
<references />{{Nouveau livre}}
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
tb12chyibg3cca0j3hgiba953lbhhs4
763013
763010
2026-04-05T21:25:21Z
ROSEMARSH HOOD
122846
ajout catégories
763013
wikitext
text/x-wiki
{{NavDébut|book=Guide des mots rares à adopter|page=Introduction|pageText=Démarrer}}{{Page de garde
| 2 = VisualEditor - Icon - Open-book-2.svg
| description = <big>'''{{Centrer|Découvrez divers mots rares et curiosités lexicales à adopter pour enrichir votre vocabulaire et donner du style à votre langage quotidien}}'''</big><br/>
'''''Guide des mots rares à adopter pour enrichir son vocabulaire''''' est un wikilivre incontournable pour les passionnés de langue française, les écrivains et les curieux des mots. Il propose une sélection soignée de termes rares, élégants ou méconnus, accompagnés de leurs définitions, origines et exemples d’usage concrets. S’appuyant notamment sur des ressources linguistiques comme le [[W:Lexique informatisé des mots insolites à étymologie latine|''LiMiEL'' (Lexique informatisé des mots insolites à étymologie latine)]], ce guide met en lumière la richesse du lexique français tout en offrant des clés pratiques pour intégrer ces mots dans son expression écrite et orale. Idéal pour enrichir son vocabulaire, affiner son style et redécouvrir la beauté des mots.
| avancement = Avancé
}}
== Source centrale du livre ==
Ce [[wikt:livre|livre]] s’appuie en grande partie sur les [[wikt:données_ouvertes|données]] du ''[[w:Lexique_informatisé_des_mots_insolites_à_étymologie_latine|Lexique informatisé des mots insolites à étymologie latine]]'', qui constitue une ressource de référence pour l’[[wikt:identification|identification]] et l’[[wikt:étude|étude]] de [[w:Mot_rare|mots rares]] en [[français]]. Les contenus issus de cette base sont utilisés conformément à leur [[Licences libres|licence]] '''[[Licences libres#Pour les données|ODC-By (Open Data Commons Attribution)]]''', qui autorise la [[wikt:réutilisation|réutilisation]], la [[wikt:modification|modification]] et la [[w:Données_ouvertes#Open_Database_Commons|diffusion des données à condition d’en mentionner la source.]] Cette approche garantit à la fois la [[w:Lexique|richesse lexicale]] du [[wikt:Modèle:R:LiMiEL|guide]] et le [[w:Licence_libre|respect]] des principes de [[wikt:données_ouvertes|partage ouvert]] des [[wikt:connaissance|connaissances]]<ref>https://limiel.omeka.net/licence_odc-by</ref>.
Un [[wikt:Wikilivres|wikilivre]] à été rédigé pour apprendre à [[wikt:consulter|naviguer]] dans ce [[wikt:dictionnaire|dictionnaire]] [[wikt:numérique|numérique]] : [[Utiliser le LiMiEL de manière productive]]
== Qu'est-ce qu'un [[w:Mot_rare|mot rare]] ? ==
Un '''[[w:Mot_rare|mot rare]]''' est une [[w:Lexème|unité lexicale]] '''peu utilisée''' dans la [[w:Français|langue française]]. Selon le ''TLFi'' (''[[w:Trésor_de_la_langue_française_informatisé|Trésor de la langue française informatisé]]''), ces mots apparaissent rarement dans les [[w:Corpus|corpus littéraires]] et dans la langue quotidienne. Les dictionnaires les signalent souvent comme '''[[wikt:ancien|anciens,]] [[wikt:littéraire|littéraires]] ou [[wikt:ancien|rares]]'''[[wikt:ancien|,]] ce qui les distingue clairement des mots courants fréquemment employés dans le français parlé et écrit<ref name=":0">Josette Rey-Debove, ''Étude linguistique et sémiotique des dictionnaires français contemporains'', Mouton, La Haye & Paris, 1971, p. 81.</ref><ref>https://usito.usherbrooke.ca/lexies/mots/rare</ref>.
== Sommaire ==
* [[Guide des mots rares à adopter/Introduction|Introduction]]
''<small>([[wikt:cliquer|cliquer]] sur le [[w:Mot_rare|mot rare]] pour lire sa fiche complète)</small>''
* [[Guide des mots rares à adopter/crépat, crépate|'''crépat, crépate''']] : qui produit un effet sonore '''bruyant mais envoûtant''', capable de captiver l’attention par son intensité harmonieuse ou rythmique ;
* [[Guide des mots rares à adopter/clangart, clangarte|'''clangart, clangarte''']] : qui produit un effet sonore '''soudainement assourdissant''', souvent perçu comme agressif, brutal ou désagréable ;
* [[Guide des mots rares à adopter/caullée|'''caullée''']] : ensemble de '''cavités ou de précipices contigus''' ;
* [[Guide des mots rares à adopter/aconcordesque|'''aconcordesque''']] : qui, dans une relation ou une situation affective, '''trahit une concordance amoureuse''', rompt une harmonie sentimentale ou va à l’encontre des élans du cœur ;
* [[Guide des mots rares à adopter/aéruminal, aéruminale|'''aéruminal, aéruminale''']] : qui est '''accablé de peine, de misères ou de tristesse profonde''' ;
* [[Guide des mots rares à adopter/audulard, audularde|'''audulard, audularde''']] : qui '''se laisse facilement flatter''', qui est particulièrement '''réceptif à l’adulation''' ou aux compliments intéressés ;
* [[Guide des mots rares à adopter/auricolore|'''auricolore''']] : qui possède '''une couleur d’or ou une teinte dorée''', brillante et éclatante ;
* [[Guide des mots rares à adopter/blatérate|'''blatérate''']] : qui '''ne mentionne rien de concret''', mais qui le donne à croire par son ton, son style ou son vocabulaire soigné ;
* [[Guide des mots rares à adopter/blatération|'''blatération''']] : '''bavardage frivole''' '''et superficiel''', suite de paroles vaines ou légères, souvent répétitives et sans réel contenu ;
* [[Guide des mots rares à adopter/burgenatif, burgenative|'''burgenatif, burgenative''']] : personne née d'une '''famille riche ou bourgeoise''' ;
* [[Guide des mots rares à adopter/emphygomphe|'''emphygomphe''']] : '''proclamation lyrique excessivement emphatique''', marquée par une exagération du ton et une importance démesurée accordée à des propos souvent creux ;
* [[Guide des mots rares à adopter/clausible|'''clausible''']] : '''qui peut se refermer ou se sceller sur soi-même sans perte d’intégrité''' ;
* [[Guide des mots rares à adopter/clausibilité|'''clausibilité''']] : capacité d’un objet, d’un système ou d’un ensemble '''à se refermer sur lui-même ou à se sceller sans perte d’intégrité''' ;
* [[Guide des mots rares à adopter/brèviloquent, brèviloquente|'''brèviloquent, brèviloquente''']] : qui se caractérise par une expression sobre, concise et mesurée, '''privilégiant la brièveté dans la parole ou l’écriture'''.
*''<small>(autres à venir)</small>''
== Pour aller plus loin ; autres ressources consacrées aux curiosités lexicales ==
=== Ouvrages ===
* Le Drouviot, ''dictionnaire des mots rares ou exceptionnels de la langue française'' [http://drouviot.net/dictionnaire → consulter cet ouvrage]
* LiMiEL, ''Lexique informatisé des mots insolites à étymologie latine'', depuis 2025 [https://limiel.omeka.net → consulter cet ouvrage]
=== Articles ===
* « Plus de 100 mots rares pour enrichir votre vocabulaire », par Adrian, dans ''La Culture Générale'', 28 janvier 2019 [https://www.laculturegenerale.com/ameliorer-vocabulaire-enrichir/ → consulter cet article]
* « 10 mots insolites de la langue française », dans ''Éditions Maison des Langues'', 24 octobre 2024 [https://www.emdl.fr/lettres/dernieres-actualites/10-mots-insolites-de-la-langue-francaise → consulter cet article]
* « Top 20 des mots rares que tout le monde devrait connaître », par Jeannou Pagure, dans ''Topito'', 31 octobre 2022 [https://www.topito.com/top-mots-rares-devrait-connaitre → consulter cet article]
* « Cinq mots rares (et précieux) que nous ferions bien d’employer », par Claire Conruyt, dans ''Le Figaro'', 5 janvier 2020 [https://www.lefigaro.fr/langue-francaise/expressions-francaises/cinq-mots-rares-et-precieux-que-nous-ferions-bien-d-employer-20200105 → consulter cet article]
== Notes et références ==
<references />{{Nouveau livre}}
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
bqoioiaf2al0s2k8m2myddj54kczbbc
763029
763013
2026-04-05T21:46:54Z
ROSEMARSH HOOD
122846
nav box
763029
wikitext
text/x-wiki
{{NavDébut|book=Guide des mots rares à adopter|page=Introduction|pageText=Débuter}}{{Page de garde
| 2 = VisualEditor - Icon - Open-book-2.svg
| description = <big>'''{{Centrer|Découvrez divers mots rares et curiosités lexicales à adopter pour enrichir votre vocabulaire et donner du style à votre langage quotidien}}'''</big><br/>
'''''Guide des mots rares à adopter pour enrichir son vocabulaire''''' est un wikilivre incontournable pour les passionnés de langue française, les écrivains et les curieux des mots. Il propose une sélection soignée de termes rares, élégants ou méconnus, accompagnés de leurs définitions, origines et exemples d’usage concrets. S’appuyant notamment sur des ressources linguistiques comme le [[W:Lexique informatisé des mots insolites à étymologie latine|''LiMiEL'' (Lexique informatisé des mots insolites à étymologie latine)]], ce guide met en lumière la richesse du lexique français tout en offrant des clés pratiques pour intégrer ces mots dans son expression écrite et orale. Idéal pour enrichir son vocabulaire, affiner son style et redécouvrir la beauté des mots.
| avancement = Avancé
}}
== Source centrale du livre ==
Ce [[wikt:livre|livre]] s’appuie en grande partie sur les [[wikt:données_ouvertes|données]] du ''[[w:Lexique_informatisé_des_mots_insolites_à_étymologie_latine|Lexique informatisé des mots insolites à étymologie latine]]'', qui constitue une ressource de référence pour l’[[wikt:identification|identification]] et l’[[wikt:étude|étude]] de [[w:Mot_rare|mots rares]] en [[français]]. Les contenus issus de cette base sont utilisés conformément à leur [[Licences libres|licence]] '''[[Licences libres#Pour les données|ODC-By (Open Data Commons Attribution)]]''', qui autorise la [[wikt:réutilisation|réutilisation]], la [[wikt:modification|modification]] et la [[w:Données_ouvertes#Open_Database_Commons|diffusion des données à condition d’en mentionner la source.]] Cette approche garantit à la fois la [[w:Lexique|richesse lexicale]] du [[wikt:Modèle:R:LiMiEL|guide]] et le [[w:Licence_libre|respect]] des principes de [[wikt:données_ouvertes|partage ouvert]] des [[wikt:connaissance|connaissances]]<ref>https://limiel.omeka.net/licence_odc-by</ref>.
Un [[wikt:Wikilivres|wikilivre]] à été rédigé pour apprendre à [[wikt:consulter|naviguer]] dans ce [[wikt:dictionnaire|dictionnaire]] [[wikt:numérique|numérique]] : [[Utiliser le LiMiEL de manière productive]]
== Qu'est-ce qu'un [[w:Mot_rare|mot rare]] ? ==
Un '''[[w:Mot_rare|mot rare]]''' est une [[w:Lexème|unité lexicale]] '''peu utilisée''' dans la [[w:Français|langue française]]. Selon le ''TLFi'' (''[[w:Trésor_de_la_langue_française_informatisé|Trésor de la langue française informatisé]]''), ces mots apparaissent rarement dans les [[w:Corpus|corpus littéraires]] et dans la langue quotidienne. Les dictionnaires les signalent souvent comme '''[[wikt:ancien|anciens,]] [[wikt:littéraire|littéraires]] ou [[wikt:ancien|rares]]'''[[wikt:ancien|,]] ce qui les distingue clairement des mots courants fréquemment employés dans le français parlé et écrit<ref name=":0">Josette Rey-Debove, ''Étude linguistique et sémiotique des dictionnaires français contemporains'', Mouton, La Haye & Paris, 1971, p. 81.</ref><ref>https://usito.usherbrooke.ca/lexies/mots/rare</ref>.
== Sommaire ==
* [[Guide des mots rares à adopter/Introduction|Introduction]]
''<small>([[wikt:cliquer|cliquer]] sur le [[w:Mot_rare|mot rare]] pour lire sa fiche complète)</small>''
* [[Guide des mots rares à adopter/crépat, crépate|'''crépat, crépate''']] : qui produit un effet sonore '''bruyant mais envoûtant''', capable de captiver l’attention par son intensité harmonieuse ou rythmique ;
* [[Guide des mots rares à adopter/clangart, clangarte|'''clangart, clangarte''']] : qui produit un effet sonore '''soudainement assourdissant''', souvent perçu comme agressif, brutal ou désagréable ;
* [[Guide des mots rares à adopter/caullée|'''caullée''']] : ensemble de '''cavités ou de précipices contigus''' ;
* [[Guide des mots rares à adopter/aconcordesque|'''aconcordesque''']] : qui, dans une relation ou une situation affective, '''trahit une concordance amoureuse''', rompt une harmonie sentimentale ou va à l’encontre des élans du cœur ;
* [[Guide des mots rares à adopter/aéruminal, aéruminale|'''aéruminal, aéruminale''']] : qui est '''accablé de peine, de misères ou de tristesse profonde''' ;
* [[Guide des mots rares à adopter/audulard, audularde|'''audulard, audularde''']] : qui '''se laisse facilement flatter''', qui est particulièrement '''réceptif à l’adulation''' ou aux compliments intéressés ;
* [[Guide des mots rares à adopter/auricolore|'''auricolore''']] : qui possède '''une couleur d’or ou une teinte dorée''', brillante et éclatante ;
* [[Guide des mots rares à adopter/blatérate|'''blatérate''']] : qui '''ne mentionne rien de concret''', mais qui le donne à croire par son ton, son style ou son vocabulaire soigné ;
* [[Guide des mots rares à adopter/blatération|'''blatération''']] : '''bavardage frivole''' '''et superficiel''', suite de paroles vaines ou légères, souvent répétitives et sans réel contenu ;
* [[Guide des mots rares à adopter/burgenatif, burgenative|'''burgenatif, burgenative''']] : personne née d'une '''famille riche ou bourgeoise''' ;
* [[Guide des mots rares à adopter/emphygomphe|'''emphygomphe''']] : '''proclamation lyrique excessivement emphatique''', marquée par une exagération du ton et une importance démesurée accordée à des propos souvent creux ;
* [[Guide des mots rares à adopter/clausible|'''clausible''']] : '''qui peut se refermer ou se sceller sur soi-même sans perte d’intégrité''' ;
* [[Guide des mots rares à adopter/clausibilité|'''clausibilité''']] : capacité d’un objet, d’un système ou d’un ensemble '''à se refermer sur lui-même ou à se sceller sans perte d’intégrité''' ;
* [[Guide des mots rares à adopter/brèviloquent, brèviloquente|'''brèviloquent, brèviloquente''']] : qui se caractérise par une expression sobre, concise et mesurée, '''privilégiant la brièveté dans la parole ou l’écriture'''.
*''<small>(autres à venir)</small>''
== Pour aller plus loin ; autres ressources consacrées aux curiosités lexicales ==
=== Ouvrages ===
* Le Drouviot, ''dictionnaire des mots rares ou exceptionnels de la langue française'' [http://drouviot.net/dictionnaire → consulter cet ouvrage]
* LiMiEL, ''Lexique informatisé des mots insolites à étymologie latine'', depuis 2025 [https://limiel.omeka.net → consulter cet ouvrage]
=== Articles ===
* « Plus de 100 mots rares pour enrichir votre vocabulaire », par Adrian, dans ''La Culture Générale'', 28 janvier 2019 [https://www.laculturegenerale.com/ameliorer-vocabulaire-enrichir/ → consulter cet article]
* « 10 mots insolites de la langue française », dans ''Éditions Maison des Langues'', 24 octobre 2024 [https://www.emdl.fr/lettres/dernieres-actualites/10-mots-insolites-de-la-langue-francaise → consulter cet article]
* « Top 20 des mots rares que tout le monde devrait connaître », par Jeannou Pagure, dans ''Topito'', 31 octobre 2022 [https://www.topito.com/top-mots-rares-devrait-connaitre → consulter cet article]
* « Cinq mots rares (et précieux) que nous ferions bien d’employer », par Claire Conruyt, dans ''Le Figaro'', 5 janvier 2020 [https://www.lefigaro.fr/langue-francaise/expressions-francaises/cinq-mots-rares-et-precieux-que-nous-ferions-bien-d-employer-20200105 → consulter cet article]
== Notes et références ==
<references />{{Nouveau livre}}
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
l7xnmsubj1fac2fftwamw52m34j6boo
763030
763029
2026-04-05T21:57:37Z
ROSEMARSH HOOD
122846
Ajout de l'image
763030
wikitext
text/x-wiki
{{NavDébut|book=Guide des mots rares à adopter|page=Introduction|pageText=Débuter}}{{Page de garde
| 2 = VisualEditor - Icon - Open-book-2.svg
| description = <big>'''{{Centrer|Découvrez divers mots rares et curiosités lexicales à adopter pour enrichir votre vocabulaire et donner du style à votre langage quotidien}}'''</big><br/>
'''''Guide des mots rares à adopter pour enrichir son vocabulaire''''' est un wikilivre incontournable pour les passionnés de langue française, les écrivains et les curieux des mots. Il propose une sélection soignée de termes rares, élégants ou méconnus, accompagnés de leurs définitions, origines et exemples d’usage concrets. S’appuyant notamment sur des ressources linguistiques comme le [[W:Lexique informatisé des mots insolites à étymologie latine|''LiMiEL'' (Lexique informatisé des mots insolites à étymologie latine)]], ce guide met en lumière la richesse du lexique français tout en offrant des clés pratiques pour intégrer ces mots dans son expression écrite et orale. Idéal pour enrichir son vocabulaire, affiner son style et redécouvrir la beauté des mots.
| avancement = Avancé
}}
== Source centrale du livre ==
Ce [[wikt:livre|livre]] s’appuie en grande partie sur les [[wikt:données_ouvertes|données]] du ''[[w:Lexique_informatisé_des_mots_insolites_à_étymologie_latine|Lexique informatisé des mots insolites à étymologie latine]]'', qui constitue une ressource de référence pour l’[[wikt:identification|identification]] et l’[[wikt:étude|étude]] de [[w:Mot_rare|mots rares]] en [[français]]. Les contenus issus de cette base sont utilisés conformément à leur [[Licences libres|licence]] '''[[Licences libres#Pour les données|ODC-By (Open Data Commons Attribution)]]''', qui autorise la [[wikt:réutilisation|réutilisation]], la [[wikt:modification|modification]] et la [[w:Données_ouvertes#Open_Database_Commons|diffusion des données à condition d’en mentionner la source.]] Cette approche garantit à la fois la [[w:Lexique|richesse lexicale]] du [[wikt:Modèle:R:LiMiEL|guide]] et le [[w:Licence_libre|respect]] des principes de [[wikt:données_ouvertes|partage ouvert]] des [[wikt:connaissance|connaissances]]<ref>https://limiel.omeka.net/licence_odc-by</ref>.
Un [[wikt:Wikilivres|wikilivre]] à été rédigé pour apprendre à [[wikt:consulter|naviguer]] dans ce [[wikt:dictionnaire|dictionnaire]] [[wikt:numérique|numérique]] : [[Utiliser le LiMiEL de manière productive]]
== Qu'est-ce qu'un [[w:Mot_rare|mot rare]] ? ==
Un '''[[w:Mot_rare|mot rare]]''' est une [[w:Lexème|unité lexicale]] '''peu utilisée''' dans la [[w:Français|langue française]]. Selon le ''TLFi'' (''[[w:Trésor_de_la_langue_française_informatisé|Trésor de la langue française informatisé]]''), ces mots apparaissent rarement dans les [[w:Corpus|corpus littéraires]] et dans la langue quotidienne. Les dictionnaires les signalent souvent comme '''[[wikt:ancien|anciens,]] [[wikt:littéraire|littéraires]] ou [[wikt:ancien|rares]]'''[[wikt:ancien|,]] ce qui les distingue clairement des mots courants fréquemment employés dans le français parlé et écrit<ref name=":0">Josette Rey-Debove, ''Étude linguistique et sémiotique des dictionnaires français contemporains'', Mouton, La Haye & Paris, 1971, p. 81.</ref><ref>https://usito.usherbrooke.ca/lexies/mots/rare</ref>.
[[Fichier:Littré_-_1863_-_A-C_-_page_941_-_définition_cyclone.jpg|vignette|Gros plan de la page 941 du ''[[w:Dictionnaire_de_la_langue_française|Dictionnaire de la langue française]]'' d’[[w:Émile_Littré|Émile Littré]], ouvrage réputé pour la richesse de son répertoire, incluant de nombreux mots aujourd’hui devenus rares.]]
== Sommaire ==
* [[Guide des mots rares à adopter/Introduction|Introduction]]
''<small>([[wikt:cliquer|cliquer]] sur le [[w:Mot_rare|mot rare]] pour lire sa fiche complète)</small>''
* [[Guide des mots rares à adopter/crépat, crépate|'''crépat, crépate''']] : qui produit un effet sonore '''bruyant mais envoûtant''', capable de captiver l’attention par son intensité harmonieuse ou rythmique ;
* [[Guide des mots rares à adopter/clangart, clangarte|'''clangart, clangarte''']] : qui produit un effet sonore '''soudainement assourdissant''', souvent perçu comme agressif, brutal ou désagréable ;
* [[Guide des mots rares à adopter/caullée|'''caullée''']] : ensemble de '''cavités ou de précipices contigus''' ;
* [[Guide des mots rares à adopter/aconcordesque|'''aconcordesque''']] : qui, dans une relation ou une situation affective, '''trahit une concordance amoureuse''', rompt une harmonie sentimentale ou va à l’encontre des élans du cœur ;
* [[Guide des mots rares à adopter/aéruminal, aéruminale|'''aéruminal, aéruminale''']] : qui est '''accablé de peine, de misères ou de tristesse profonde''' ;
* [[Guide des mots rares à adopter/audulard, audularde|'''audulard, audularde''']] : qui '''se laisse facilement flatter''', qui est particulièrement '''réceptif à l’adulation''' ou aux compliments intéressés ;
* [[Guide des mots rares à adopter/auricolore|'''auricolore''']] : qui possède '''une couleur d’or ou une teinte dorée''', brillante et éclatante ;
* [[Guide des mots rares à adopter/blatérate|'''blatérate''']] : qui '''ne mentionne rien de concret''', mais qui le donne à croire par son ton, son style ou son vocabulaire soigné ;
* [[Guide des mots rares à adopter/blatération|'''blatération''']] : '''bavardage frivole''' '''et superficiel''', suite de paroles vaines ou légères, souvent répétitives et sans réel contenu ;
* [[Guide des mots rares à adopter/burgenatif, burgenative|'''burgenatif, burgenative''']] : personne née d'une '''famille riche ou bourgeoise''' ;
* [[Guide des mots rares à adopter/emphygomphe|'''emphygomphe''']] : '''proclamation lyrique excessivement emphatique''', marquée par une exagération du ton et une importance démesurée accordée à des propos souvent creux ;
* [[Guide des mots rares à adopter/clausible|'''clausible''']] : '''qui peut se refermer ou se sceller sur soi-même sans perte d’intégrité''' ;
* [[Guide des mots rares à adopter/clausibilité|'''clausibilité''']] : capacité d’un objet, d’un système ou d’un ensemble '''à se refermer sur lui-même ou à se sceller sans perte d’intégrité''' ;
* [[Guide des mots rares à adopter/brèviloquent, brèviloquente|'''brèviloquent, brèviloquente''']] : qui se caractérise par une expression sobre, concise et mesurée, '''privilégiant la brièveté dans la parole ou l’écriture'''.
*''<small>(autres à venir)</small>''
== Pour aller plus loin ; autres ressources consacrées aux curiosités lexicales ==
=== Ouvrages ===
* Le Drouviot, ''dictionnaire des mots rares ou exceptionnels de la langue française'' [http://drouviot.net/dictionnaire → consulter cet ouvrage]
* LiMiEL, ''Lexique informatisé des mots insolites à étymologie latine'', depuis 2025 [https://limiel.omeka.net → consulter cet ouvrage]
=== Articles ===
* « Plus de 100 mots rares pour enrichir votre vocabulaire », par Adrian, dans ''La Culture Générale'', 28 janvier 2019 [https://www.laculturegenerale.com/ameliorer-vocabulaire-enrichir/ → consulter cet article]
* « 10 mots insolites de la langue française », dans ''Éditions Maison des Langues'', 24 octobre 2024 [https://www.emdl.fr/lettres/dernieres-actualites/10-mots-insolites-de-la-langue-francaise → consulter cet article]
* « Top 20 des mots rares que tout le monde devrait connaître », par Jeannou Pagure, dans ''Topito'', 31 octobre 2022 [https://www.topito.com/top-mots-rares-devrait-connaitre → consulter cet article]
* « Cinq mots rares (et précieux) que nous ferions bien d’employer », par Claire Conruyt, dans ''Le Figaro'', 5 janvier 2020 [https://www.lefigaro.fr/langue-francaise/expressions-francaises/cinq-mots-rares-et-precieux-que-nous-ferions-bien-d-employer-20200105 → consulter cet article]
== Notes et références ==
<references />{{Nouveau livre}}
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
7zte2ta415rg8w4trly0agu6eqjub8i
763031
763030
2026-04-05T22:03:57Z
ROSEMARSH HOOD
122846
/* Source centrale du livre */
763031
wikitext
text/x-wiki
{{NavDébut|book=Guide des mots rares à adopter|page=Introduction|pageText=Débuter}}{{Page de garde
| 2 = VisualEditor - Icon - Open-book-2.svg
| description = <big>'''{{Centrer|Découvrez divers mots rares et curiosités lexicales à adopter pour enrichir votre vocabulaire et donner du style à votre langage quotidien}}'''</big><br/>
'''''Guide des mots rares à adopter pour enrichir son vocabulaire''''' est un wikilivre incontournable pour les passionnés de langue française, les écrivains et les curieux des mots. Il propose une sélection soignée de termes rares, élégants ou méconnus, accompagnés de leurs définitions, origines et exemples d’usage concrets. S’appuyant notamment sur des ressources linguistiques comme le [[W:Lexique informatisé des mots insolites à étymologie latine|''LiMiEL'' (Lexique informatisé des mots insolites à étymologie latine)]], ce guide met en lumière la richesse du lexique français tout en offrant des clés pratiques pour intégrer ces mots dans son expression écrite et orale. Idéal pour enrichir son vocabulaire, affiner son style et redécouvrir la beauté des mots.
| avancement = Avancé
}}
== Source centrale du livre ==
Ce [[wikt:livre|livre]] s’appuie en grande partie sur les [[wikt:données_ouvertes|données]] du ''[[w:Lexique_informatisé_des_mots_insolites_à_étymologie_latine|Lexique informatisé des mots insolites à étymologie latine]]'', qui constitue une ressource de référence pour l’[[wikt:identification|identification]] et l’[[wikt:étude|étude]] de [[w:Mot_rare|mots rares]] en [[français]]. Les contenus issus de cette base sont utilisés conformément à leur [[Licences libres|licence]] '''[[Licences libres#Pour les données|ODC-By (Open Data Commons Attribution)]]''', qui autorise la [[wikt:réutilisation|réutilisation]], la [[wikt:modification|modification]] et la [[w:Données_ouvertes#Open_Database_Commons|diffusion des données à condition d’en mentionner la source.]] Cette approche garantit à la fois la [[w:Lexique|richesse lexicale]] du [[wikt:Modèle:R:LiMiEL|guide]] et le [[w:Licence_libre|respect]] des principes de [[wikt:données_ouvertes|partage ouvert]] des [[wikt:connaissance|connaissances]]<ref>https://limiel.omeka.net/licence_odc-by</ref>.
Un [[wikt:Wikilivres|wikilivre]] à été rédigé pour apprendre à [[wikt:consulter|naviguer]] dans ce [[wikt:dictionnaire|dictionnaire]] [[wikt:numérique|numérique]] : [[Utiliser le LiMiEL de manière productive]]
== Qu'est-ce qu'un [[w:Mot_rare|mot rare]] ? ==
{{Autres projets|w=Mot rare}}Un '''[[w:Mot_rare|mot rare]]''' est une [[w:Lexème|unité lexicale]] '''peu utilisée''' dans la [[w:Français|langue française]]. Selon le ''TLFi'' (''[[w:Trésor_de_la_langue_française_informatisé|Trésor de la langue française informatisé]]''), ces mots apparaissent rarement dans les [[w:Corpus|corpus littéraires]] et dans la langue quotidienne. Les dictionnaires les signalent souvent comme '''[[wikt:ancien|anciens,]] [[wikt:littéraire|littéraires]] ou [[wikt:ancien|rares]]'''[[wikt:ancien|,]] ce qui les distingue clairement des mots courants fréquemment employés dans le français parlé et écrit<ref name=":0">Josette Rey-Debove, ''Étude linguistique et sémiotique des dictionnaires français contemporains'', Mouton, La Haye & Paris, 1971, p. 81.</ref><ref>https://usito.usherbrooke.ca/lexies/mots/rare</ref>.
[[Fichier:Littré_-_1863_-_A-C_-_page_941_-_définition_cyclone.jpg|vignette|Gros plan de la page 941 du ''[[w:Dictionnaire_de_la_langue_française|Dictionnaire de la langue française]]'' d’[[w:Émile_Littré|Émile Littré]], ouvrage réputé pour la richesse de son répertoire, incluant de nombreux mots aujourd’hui devenus rares.]]
== Sommaire ==
* [[Guide des mots rares à adopter/Introduction|Introduction]]
''<small>([[wikt:cliquer|cliquer]] sur le [[w:Mot_rare|mot rare]] pour lire sa fiche complète)</small>''
* [[Guide des mots rares à adopter/crépat, crépate|'''crépat, crépate''']] : qui produit un effet sonore '''bruyant mais envoûtant''', capable de captiver l’attention par son intensité harmonieuse ou rythmique ;
* [[Guide des mots rares à adopter/clangart, clangarte|'''clangart, clangarte''']] : qui produit un effet sonore '''soudainement assourdissant''', souvent perçu comme agressif, brutal ou désagréable ;
* [[Guide des mots rares à adopter/caullée|'''caullée''']] : ensemble de '''cavités ou de précipices contigus''' ;
* [[Guide des mots rares à adopter/aconcordesque|'''aconcordesque''']] : qui, dans une relation ou une situation affective, '''trahit une concordance amoureuse''', rompt une harmonie sentimentale ou va à l’encontre des élans du cœur ;
* [[Guide des mots rares à adopter/aéruminal, aéruminale|'''aéruminal, aéruminale''']] : qui est '''accablé de peine, de misères ou de tristesse profonde''' ;
* [[Guide des mots rares à adopter/audulard, audularde|'''audulard, audularde''']] : qui '''se laisse facilement flatter''', qui est particulièrement '''réceptif à l’adulation''' ou aux compliments intéressés ;
* [[Guide des mots rares à adopter/auricolore|'''auricolore''']] : qui possède '''une couleur d’or ou une teinte dorée''', brillante et éclatante ;
* [[Guide des mots rares à adopter/blatérate|'''blatérate''']] : qui '''ne mentionne rien de concret''', mais qui le donne à croire par son ton, son style ou son vocabulaire soigné ;
* [[Guide des mots rares à adopter/blatération|'''blatération''']] : '''bavardage frivole''' '''et superficiel''', suite de paroles vaines ou légères, souvent répétitives et sans réel contenu ;
* [[Guide des mots rares à adopter/burgenatif, burgenative|'''burgenatif, burgenative''']] : personne née d'une '''famille riche ou bourgeoise''' ;
* [[Guide des mots rares à adopter/emphygomphe|'''emphygomphe''']] : '''proclamation lyrique excessivement emphatique''', marquée par une exagération du ton et une importance démesurée accordée à des propos souvent creux ;
* [[Guide des mots rares à adopter/clausible|'''clausible''']] : '''qui peut se refermer ou se sceller sur soi-même sans perte d’intégrité''' ;
* [[Guide des mots rares à adopter/clausibilité|'''clausibilité''']] : capacité d’un objet, d’un système ou d’un ensemble '''à se refermer sur lui-même ou à se sceller sans perte d’intégrité''' ;
* [[Guide des mots rares à adopter/brèviloquent, brèviloquente|'''brèviloquent, brèviloquente''']] : qui se caractérise par une expression sobre, concise et mesurée, '''privilégiant la brièveté dans la parole ou l’écriture'''.
*''<small>(autres à venir)</small>''
== Pour aller plus loin ; autres ressources consacrées aux curiosités lexicales ==
=== Ouvrages ===
* Le Drouviot, ''dictionnaire des mots rares ou exceptionnels de la langue française'' [http://drouviot.net/dictionnaire → consulter cet ouvrage]
* LiMiEL, ''Lexique informatisé des mots insolites à étymologie latine'', depuis 2025 [https://limiel.omeka.net → consulter cet ouvrage]
=== Articles ===
* « Plus de 100 mots rares pour enrichir votre vocabulaire », par Adrian, dans ''La Culture Générale'', 28 janvier 2019 [https://www.laculturegenerale.com/ameliorer-vocabulaire-enrichir/ → consulter cet article]
* « 10 mots insolites de la langue française », dans ''Éditions Maison des Langues'', 24 octobre 2024 [https://www.emdl.fr/lettres/dernieres-actualites/10-mots-insolites-de-la-langue-francaise → consulter cet article]
* « Top 20 des mots rares que tout le monde devrait connaître », par Jeannou Pagure, dans ''Topito'', 31 octobre 2022 [https://www.topito.com/top-mots-rares-devrait-connaitre → consulter cet article]
* « Cinq mots rares (et précieux) que nous ferions bien d’employer », par Claire Conruyt, dans ''Le Figaro'', 5 janvier 2020 [https://www.lefigaro.fr/langue-francaise/expressions-francaises/cinq-mots-rares-et-precieux-que-nous-ferions-bien-d-employer-20200105 → consulter cet article]
== Notes et références ==
<references />{{Nouveau livre}}
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
54lorgwr680o27zufut7hbcpefstat4
763037
763031
2026-04-05T23:56:40Z
ROSEMARSH HOOD
122846
/* Sommaire */ mise à jour
763037
wikitext
text/x-wiki
{{NavDébut|book=Guide des mots rares à adopter|page=Introduction|pageText=Débuter}}{{Page de garde
| 2 = VisualEditor - Icon - Open-book-2.svg
| description = <big>'''{{Centrer|Découvrez divers mots rares et curiosités lexicales à adopter pour enrichir votre vocabulaire et donner du style à votre langage quotidien}}'''</big><br/>
'''''Guide des mots rares à adopter pour enrichir son vocabulaire''''' est un wikilivre incontournable pour les passionnés de langue française, les écrivains et les curieux des mots. Il propose une sélection soignée de termes rares, élégants ou méconnus, accompagnés de leurs définitions, origines et exemples d’usage concrets. S’appuyant notamment sur des ressources linguistiques comme le [[W:Lexique informatisé des mots insolites à étymologie latine|''LiMiEL'' (Lexique informatisé des mots insolites à étymologie latine)]], ce guide met en lumière la richesse du lexique français tout en offrant des clés pratiques pour intégrer ces mots dans son expression écrite et orale. Idéal pour enrichir son vocabulaire, affiner son style et redécouvrir la beauté des mots.
| avancement = Avancé
}}
== Source centrale du livre ==
Ce [[wikt:livre|livre]] s’appuie en grande partie sur les [[wikt:données_ouvertes|données]] du ''[[w:Lexique_informatisé_des_mots_insolites_à_étymologie_latine|Lexique informatisé des mots insolites à étymologie latine]]'', qui constitue une ressource de référence pour l’[[wikt:identification|identification]] et l’[[wikt:étude|étude]] de [[w:Mot_rare|mots rares]] en [[français]]. Les contenus issus de cette base sont utilisés conformément à leur [[Licences libres|licence]] '''[[Licences libres#Pour les données|ODC-By (Open Data Commons Attribution)]]''', qui autorise la [[wikt:réutilisation|réutilisation]], la [[wikt:modification|modification]] et la [[w:Données_ouvertes#Open_Database_Commons|diffusion des données à condition d’en mentionner la source.]] Cette approche garantit à la fois la [[w:Lexique|richesse lexicale]] du [[wikt:Modèle:R:LiMiEL|guide]] et le [[w:Licence_libre|respect]] des principes de [[wikt:données_ouvertes|partage ouvert]] des [[wikt:connaissance|connaissances]]<ref>https://limiel.omeka.net/licence_odc-by</ref>.
Un [[wikt:Wikilivres|wikilivre]] à été rédigé pour apprendre à [[wikt:consulter|naviguer]] dans ce [[wikt:dictionnaire|dictionnaire]] [[wikt:numérique|numérique]] : [[Utiliser le LiMiEL de manière productive]]
== Qu'est-ce qu'un [[w:Mot_rare|mot rare]] ? ==
{{Autres projets|w=Mot rare}}Un '''[[w:Mot_rare|mot rare]]''' est une [[w:Lexème|unité lexicale]] '''peu utilisée''' dans la [[w:Français|langue française]]. Selon le ''TLFi'' (''[[w:Trésor_de_la_langue_française_informatisé|Trésor de la langue française informatisé]]''), ces mots apparaissent rarement dans les [[w:Corpus|corpus littéraires]] et dans la langue quotidienne. Les dictionnaires les signalent souvent comme '''[[wikt:ancien|anciens,]] [[wikt:littéraire|littéraires]] ou [[wikt:ancien|rares]]'''[[wikt:ancien|,]] ce qui les distingue clairement des mots courants fréquemment employés dans le français parlé et écrit<ref name=":0">Josette Rey-Debove, ''Étude linguistique et sémiotique des dictionnaires français contemporains'', Mouton, La Haye & Paris, 1971, p. 81.</ref><ref>https://usito.usherbrooke.ca/lexies/mots/rare</ref>.
[[Fichier:Littré_-_1863_-_A-C_-_page_941_-_définition_cyclone.jpg|vignette|Gros plan de la page 941 du ''[[w:Dictionnaire_de_la_langue_française|Dictionnaire de la langue française]]'' d’[[w:Émile_Littré|Émile Littré]], ouvrage réputé pour la richesse de son répertoire, incluant de nombreux mots aujourd’hui devenus rares.]]
== Sommaire ==
* [[Guide des mots rares à adopter/Introduction|Introduction]]
''<small>([[wikt:cliquer|cliquer]] sur le [[w:Mot_rare|mot rare]] pour lire sa fiche complète)</small>''
* [[Guide des mots rares à adopter/crépat, crépate|'''crépat, crépate''']] : qui produit un effet sonore '''bruyant mais envoûtant''', capable de captiver l’attention par son intensité harmonieuse ou rythmique ;
* [[Guide des mots rares à adopter/clangart, clangarte|'''clangart, clangarte''']] : qui produit un effet sonore '''soudainement assourdissant''', souvent perçu comme agressif, brutal ou désagréable ;
* [[Guide des mots rares à adopter/caullée|'''caullée''']] : ensemble de '''cavités ou de précipices contigus''' ;
* [[Guide des mots rares à adopter/aconcordesque|'''aconcordesque''']] : qui, dans une relation ou une situation affective, '''trahit une concordance amoureuse''', rompt une harmonie sentimentale ou va à l’encontre des élans du cœur ;
* [[Guide des mots rares à adopter/aéruminal, aéruminale|'''aéruminal, aéruminale''']] : qui est '''accablé de peine, de misères ou de tristesse profonde''' ;
* [[Guide des mots rares à adopter/audulard, audularde|'''audulard, audularde''']] : qui '''se laisse facilement flatter''', qui est particulièrement '''réceptif à l’adulation''' ou aux compliments intéressés ;
* [[Guide des mots rares à adopter/auricolore|'''auricolore''']] : qui possède '''une couleur d’or ou une teinte dorée''', brillante et éclatante ;
* [[Guide des mots rares à adopter/blatérate|'''blatérate''']] : qui '''ne mentionne rien de concret''', mais qui le donne à croire par son ton, son style ou son vocabulaire soigné ;
* [[Guide des mots rares à adopter/blatération|'''blatération''']] : '''bavardage frivole''' '''et superficiel''', suite de paroles vaines ou légères, souvent répétitives et sans réel contenu ;
* [[Guide des mots rares à adopter/burgenatif, burgenative|'''burgenatif, burgenative''']] : personne née d'une '''famille riche ou bourgeoise''' ;
* [[Guide des mots rares à adopter/emphygomphe|'''emphygomphe''']] : '''proclamation lyrique excessivement emphatique''', marquée par une exagération du ton et une importance démesurée accordée à des propos souvent creux ;
* [[Guide des mots rares à adopter/clausible|'''clausible''']] : '''qui peut se refermer ou se sceller sur soi-même sans perte d’intégrité''' ;
* [[Guide des mots rares à adopter/clausibilité|'''clausibilité''']] : capacité d’un objet, d’un système ou d’un ensemble '''à se refermer sur lui-même ou à se sceller sans perte d’intégrité''' ;
* [[Guide des mots rares à adopter/brèviloquent, brèviloquente|'''brèviloquent, brèviloquente''']] : qui se caractérise par une expression sobre, concise et mesurée, '''privilégiant la brièveté dans la parole ou l’écriture''' ;
* '''[[Guide des mots rares à adopter/culicellique|culicellique]]''' : qui rappelle par sa finesse, sa légèreté ou sa délicatesse '''l’aspect ou le comportement d’un tout petit insecte volant''' ;
* '''[[Guide des mots rares à adopter/imbrigène|imbrigène]]''' : qui est '''né de la pluie ou a été façonné par les gouttes d’eau''' ;
* [[Guide des mots rares à adopter/fâtiloque|'''fâtiloque''']] : qui '''prédit l’avenir ou connaît le destin''' ;
* '''[[Guide des mots rares à adopter/fatilégue|fatilégue]]''' : qui '''récolte les âmes ou collectionne les morts'''.
*''<small>(autres à venir)</small>''
== Pour aller plus loin ; autres ressources consacrées aux curiosités lexicales ==
=== Ouvrages ===
* Le Drouviot, ''dictionnaire des mots rares ou exceptionnels de la langue française'' [http://drouviot.net/dictionnaire → consulter cet ouvrage]
* LiMiEL, ''Lexique informatisé des mots insolites à étymologie latine'', depuis 2025 [https://limiel.omeka.net → consulter cet ouvrage]
=== Articles ===
* « Plus de 100 mots rares pour enrichir votre vocabulaire », par Adrian, dans ''La Culture Générale'', 28 janvier 2019 [https://www.laculturegenerale.com/ameliorer-vocabulaire-enrichir/ → consulter cet article]
* « 10 mots insolites de la langue française », dans ''Éditions Maison des Langues'', 24 octobre 2024 [https://www.emdl.fr/lettres/dernieres-actualites/10-mots-insolites-de-la-langue-francaise → consulter cet article]
* « Top 20 des mots rares que tout le monde devrait connaître », par Jeannou Pagure, dans ''Topito'', 31 octobre 2022 [https://www.topito.com/top-mots-rares-devrait-connaitre → consulter cet article]
* « Cinq mots rares (et précieux) que nous ferions bien d’employer », par Claire Conruyt, dans ''Le Figaro'', 5 janvier 2020 [https://www.lefigaro.fr/langue-francaise/expressions-francaises/cinq-mots-rares-et-precieux-que-nous-ferions-bien-d-employer-20200105 → consulter cet article]
== Notes et références ==
<references />{{Nouveau livre}}
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
16dq0nix5x3jjfiff9zvxihe42fvlw3
Guide des mots rares à adopter/crépat, crépate
0
83749
763015
762051
2026-04-05T21:32:24Z
ROSEMARSH HOOD
122846
Sommaire retour
763015
wikitext
text/x-wiki
== crépat, crépate ==
''Adjectif''
'''Définition :'''
Se dit de ce qui produit un effet [[wikt:sonore|sonore]] '''[[wikt:bruyant|bruyant]] mais [[wikt:envoûtant|envoûtant]]''', capable de captiver l’attention par son [[wikt:intensité|intensité]] [[wikt:harmonieuse|harmonieuse]] ou [[wikt:rythmique|rythmique]]. Le terme ''crépat'' évoque une sonorité [[wikt:vivante|vivante]], [[wikt:vibrante|vibrante]], presque [[wikt:hypnotique|hypnotique]], dont le bruit devient source de [[wikt:fascination|fascination]] plutôt que de [[wikt:gêne|gêne]]<ref>https://limiel.omeka.net/items/show/43</ref>.
''Exemple :'' La musique classique est crépate à mon égard.
=== Antonymie : crépat vs clangart ===
Le mot ''crépat'' s’oppose directement à ''[[Guide des mots rares à adopter/clangart, clangarte|clangart]]'', adjectif qui qualifie une sonorité '''soudainement assourdissante''', souvent perçue comme agressive ou perturbatrice. Là où ''crépat'' suggère un bruit maîtrisé, séduisant et immersif, ''clangart''renvoie à une rupture sonore brutale qui altère l’ambiance.
Ainsi, une ambiance sonore peut être dite ''crépate'' lorsqu’elle charme et enveloppe, tandis qu’elle devient ''clangarte''lorsqu’elle dérange ou rompt l’harmonie.
=== Étymologie ===
Du latin ''crepare'' (« faire du bruit, craquer »), ici réinterprété dans une perspective positive, où le bruit devient source d’émotion esthétique.
=== Source ===
[https://limiel.omeka.net/items/show/43 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 25 décembre 2025]
<references /><small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
o7pw89n3hui3ouacc3z9zfe2h8qgczq
Guide des mots rares à adopter/Introduction
0
83750
763011
762052
2026-04-05T21:23:12Z
ROSEMARSH HOOD
122846
ajout liens
763011
wikitext
text/x-wiki
{{NavDébut|book=Guide des mots rares à adopter#Sommaire|pageText=Retour au sommaire}}
== Introduction ==
[[Français|La langue française]] est un territoire vaste, parfois familier, parfois mystérieux — une véritable ''[[Guide des mots rares à adopter/caullée|caullée]]'' de mots, faite d’aspérités, de creux oubliés et de sommets encore inexplorés. Au fil du temps, certains termes se sont imposés dans l’usage courant, tandis que d’autres, [[w:Mot_rare|plus rares]], plus discrets, ont peu à peu glissé dans l’ombre. Pourtant, ce sont souvent ces mots oubliés ou méconnus qui portent en eux une richesse singulière, une précision et une poésie que le langage quotidien ne suffit pas toujours à exprimer.
Ce guide propose justement de partir à leur rencontre. Il invite le lecteur à ''exquirer'' la langue, à l’examiner avec attention, [[wikt:curiosité|curiosité]] et sens du détail, afin d’en révéler les nuances les plus fines. Car enrichir son [[Français/Littérature|vocabulaire]] ne consiste pas seulement à accumuler des mots, mais à affiner sa manière de penser, de ressentir et de dire le monde.
À travers ces pages, vous découvrirez des termes [[w:Mot_rare|rares]], parfois anciens, parfois [[w:Néologisme|nouvellement formés]], tous choisis pour leur capacité à élargir l’expression et à éveiller l’imaginaire. Chacun d’eux est une porte entrouverte sur une idée, une sensation ou une image plus précise, plus nuancée, plus vivante.
Explorer les mots rares, c’est aussi redonner une voix à ce qui semblait perdu, et réinvestir un patrimoine linguistique en constante évolution. Que vous soyez [[w:Écrivain|écrivain]], étudiant, passionné de langue ou simple curieux, ce [[wikt:livre|livre]] vous propose une invitation : celle de redécouvrir le plaisir des mots, et peut-être, de faire vôtres ceux que l’on n’entend presque plus.
<small>< [[Guide des mots rares à adopter|Retour au sommaire]]</small>
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
m4cyjks79vxst1fg9dm66kxm0xntndj
763012
763011
2026-04-05T21:24:24Z
ROSEMARSH HOOD
122846
.
763012
wikitext
text/x-wiki
{{NavDébut|book=Guide des mots rares à adopter#Sommaire|pageText=Retour au sommaire}}
== Introduction ==
[[Français|La langue française]] est un territoire vaste, parfois familier, parfois mystérieux — une véritable ''[[Guide des mots rares à adopter/caullée|caullée]]'' de mots, faite d’aspérités, de creux oubliés et de sommets encore inexplorés. Au fil du temps, certains termes se sont imposés dans l’usage courant, tandis que d’autres, [[w:Mot_rare|plus rares]], plus discrets, ont peu à peu glissé dans l’ombre. Pourtant, ce sont souvent ces mots oubliés ou méconnus qui portent en eux une richesse singulière, une précision et une poésie que le langage quotidien ne suffit pas toujours à exprimer.
Ce guide propose justement de partir à leur rencontre. Il invite le lecteur à ''exquirer'' la langue, à l’examiner avec attention, [[wikt:curiosité|curiosité]] et sens du détail, afin d’en révéler les nuances les plus fines. Car enrichir son [[Français/Littérature|vocabulaire]] ne consiste pas seulement à accumuler des mots, mais à affiner sa manière de penser, de ressentir et de dire le monde.
À travers ces pages, vous découvrirez des termes [[w:Mot_rare|rares]], parfois anciens, parfois [[w:Néologisme|nouvellement formés]], tous choisis pour leur capacité à élargir l’expression et à éveiller l’imaginaire. Chacun d’eux est une porte entrouverte sur une idée, une sensation ou une image plus précise, plus nuancée, plus vivante.
Explorer les mots rares, c’est aussi redonner une voix à ce qui semblait perdu, et réinvestir un patrimoine linguistique en constante évolution. Que vous soyez [[w:Écrivain|écrivain]], étudiant, passionné de langue ou simple curieux, ce [[wikt:livre|livre]] vous propose une invitation : celle de redécouvrir le plaisir des mots, et peut-être, de faire vôtres ceux que l’on n’entend presque plus.
<small>< [[Guide des mots rares à adopter#Sommaire|Accéder au sommaire]]</small>
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
6fymnml3rxjidx4cwr53yuzxg4xy5pu
Guide des mots rares à adopter/clangart, clangarte
0
83751
763016
762050
2026-04-05T21:35:40Z
ROSEMARSH HOOD
122846
763016
wikitext
text/x-wiki
== clangart, clangarte ==
''Adjectif''
'''Définition :'''
Se dit de ce qui produit un effet [[wikt:sonore|sonore]] '''[[wikt:soudainement|soudainement]] [[wikt:assourdissant|assourdissant]]''', souvent perçu comme [[wikt:agressif|agressif]], [[wikt:brutal|brutal]] ou [[wikt:désagréable|désagréable]]. Le terme ''clangart'' évoque une rupture sonore nette, un bruit qui surgit et perturbe une ambiance jusque-là paisible ou harmonieuse<ref>https://limiel.omeka.net/items/show/62</ref>.
''Exemple :'' Une sonorité clangarte a interrompu le concert.
=== Antonymie : clangart vs crépat ===
Le mot ''clangart'' s’oppose directement à ''[[Guide des mots rares à adopter/crépat, crépate|crépat]]'', adjectif qui qualifie une sonorité '''bruyante mais envoûtante''', capable de séduire malgré son intensité. Là où ''clangart'' renvoie à un bruit dérangeant et désagréable, ''crépat'' suggère au contraire une expérience sonore immersive et captivante.
Ainsi, une ambiance sonore est dite ''clangarte'' lorsqu’elle devient intrusive et désagréable, tandis qu’elle est ''crépate''lorsqu’elle charme et retient l’attention.
=== Étymologie ===
Du latin ''clangor'' (« son éclatant, bruit retentissant »), dont le sens est ici accentué pour souligner le caractère abrupt et perturbateur du son.
=== Source ===
[https://limiel.omeka.net/items/show/62 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 26 décembre 2025]
<references /><small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
p0t4sd7fdvp87lb5vsvn23bnmvdb0am
Guide des mots rares à adopter/aconcordesque
0
83752
763018
762049
2026-04-05T21:38:18Z
ROSEMARSH HOOD
122846
763018
wikitext
text/x-wiki
== aconcordesque ==
''Adjectif''
'''Définition :'''
Se dit de ce qui, dans une relation ou une situation affective, '''trahit une concordance amoureuse''', rompt une harmonie sentimentale ou va à l’encontre des élans du cœur. Par extension, ''aconcordesque'' qualifie une attitude, une décision ou un comportement qui '''s’oppose aux désirs amoureux''' ou aux affinités naturelles entre deux êtres<ref>https://limiel.omeka.net/items/show/13</ref>.
''Exemple :'' Elle l’eut laissé de manière aconcordesque pour répondre aux dogmes de l’orthodoxie.
=== Champ sémantique et usage ===
Le terme ''aconcordesque'' appartient au champ des dissonances affectives et des ruptures sentimentales. Il permet de décrire avec finesse ces moments où les sentiments, bien que présents, sont contredits par des choix, des contraintes ou des principes extérieurs.
Une décision est dite ''aconcordesque'' lorsqu’elle va à l’encontre de l’amour ressenti, brisant une harmonie émotionnelle pourtant évidente.
=== Intérêt linguistique ===
Le mot ''aconcordesque'' revêt un intérêt particulier pour la langue française en ce qu’il permet d’exprimer, en un seul terme, une réalité affective complexe : '''le conflit entre amour et contrainte'''. Là où le français courant nécessite souvent des formulations longues (''agir contre ses sentiments'', ''renoncer à un amour sincère''), ce néologisme offre une alternative concise, élégante et nuancée.
Sa construction hybride, associant le préfixe privatif ''a-'' à la racine latine ''concordia'' (harmonie, accord) et à la terminaison expressive ''-esque,'' renforce son pouvoir évocateur. Il s’inscrit ainsi dans une tradition de mots capables de '''saisir les subtilités de l’expérience amoureuse''', enrichissant le lexique des émotions et des relations humaines.
=== Étymologie ===
Du latin ''concordia'' (« accord, harmonie »), précédé du préfixe grec ''a-'' (privation), et enrichi d’une formation adjectivale en ''-esque''.
=== Source ===
[https://limiel.omeka.net/items/show/13 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 21 décembre 2025]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
mlq4mjfdogk94kyncc4nuxapjjs2uyc
Guide des mots rares à adopter/aéruminal, aéruminale
0
83753
763019
762048
2026-04-05T21:38:41Z
ROSEMARSH HOOD
122846
763019
wikitext
text/x-wiki
== aéruminal, aéruminale ==
''Adjectif''
'''Définition :'''
Se dit de ce qui est '''accablé de peine, de misères ou de tristesse profonde'''. Par extension, ''aéruminal'' qualifie un cœur, un esprit ou une situation '''saturés de détresses ou de douleurs affectives'''<ref>https://limiel.omeka.net/items/show/93</ref>.
''Exemple :'' Mon cœur si aéruminal.
'''Note d’usage :''' devient ''aéruminaux'' lorsqu’accordé au masculin pluriel.
=== Champ sémantique et usage ===
Le terme ''aéruminal'' s’inscrit dans le champ des émotions intenses et de la souffrance intérieure. Il est particulièrement adapté pour décrire des états de mélancolie, de désespoir ou de peine amoureuse, offrant un mot précis là où le français courant recourt à des périphrases longues.
Une personne ou un cœur est dit ''aéruminal'' lorsqu’il est profondément submergé par la tristesse ou les contrariétés affectives.
=== Intérêt linguistique ===
''aéruminal'' présente un intérêt majeur pour la langue française, car il '''permet d’exprimer en un seul terme la saturation affective par la peine ou la misère'''. Sa formation, dérivée du latin ''aerumnosus'' (« accablé de malheur »), enrichit le vocabulaire des émotions intenses et des états psychologiques profonds.
En littérature ou dans l’écriture poétique, ''aéruminal'' offre une précision émotionnelle rare, capable de rendre en un mot des nuances de douleur, de mélancolie ou de détresse sentimentale que le langage ordinaire peine à transmettre.
=== Étymologie ===
Du latin ''aerumnosus'' (« accablé de malheur, de peine »), francisé pour créer un adjectif expressif et littéraire.
=== Source ===
[https://limiel.omeka.net/items/show/93 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 27 décembre 2025]
<references /><small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
ifdoi045gh341jotynhckxviqb98ejo
Guide des mots rares à adopter/audulard, audularde
0
83754
763020
762047
2026-04-05T21:39:12Z
ROSEMARSH HOOD
122846
763020
wikitext
text/x-wiki
== audulard, audularde ==
''Adjectif''
'''Définition :'''
Se dit de quelqu’un qui '''se laisse facilement flatter''', qui est particulièrement '''réceptif à l’adulation''' ou aux compliments intéressés. Par extension, ''audulard'' qualifie une personne '''sensible à la cajolerie ou aux louanges superficielles'''<ref>https://limiel.omeka.net/items/show/5</ref>.
''Exemple :'' Une femme très audularde.
=== Champ sémantique et usage ===
Le terme ''audulard'' s’inscrit dans le champ des '''traits de caractère liés à la susceptibilité et à la vanité'''. Il permet de désigner avec précision une personne qui se laisse séduire par les flatteries, sans recourir aux périphrases longues comme ''facilement influençable par les compliments''.
Une attitude est dite ''audularde'' lorsqu’une louange ou une adulation agit directement sur le comportement ou l’enthousiasme d’une personne.
=== Intérêt linguistique ===
Le mot ''audulard'' présente un intérêt particulier pour la langue française car il '''condense en un seul terme une nuance psychologique précise'''. Sa construction, issue du latin ''adulabilis'' (dérivé de ''adulor'', « flatter »), permet de rendre accessible à l’expression littéraire ou analytique la notion de réceptivité aux louanges.
En enrichissant le vocabulaire français, ''audulard'' offre ainsi une alternative élégante aux termes génériques comme ''flatteur'', ''complaisant'' ou ''sensible aux compliments'', tout en apportant une dimension stylistique et nuancée.
=== Étymologie ===
Du latin ''adulabilis'', dérivé de ''adulor'' (« flatter »), francisé pour devenir un adjectif expressif et littéraire.
=== Source ===
[https://limiel.omeka.net/items/show/5 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 15 décembre 2025]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
69gxixb199i7s9vd16296d0ksbmdv7h
Guide des mots rares à adopter/auricolore
0
83755
763021
762053
2026-04-05T21:39:42Z
ROSEMARSH HOOD
122846
/* Source */
763021
wikitext
text/x-wiki
== auricolore ==
''Adjectif''
'''Définition :'''
Se dit de ce qui possède '''une couleur d’or ou une teinte dorée''', brillante et éclatante. Par extension, ''auricolore'' qualifie tout objet, matériau ou élément présentant une '''nuance chaude et lumineuse rappelant l’or'''<ref>https://limiel.omeka.net/items/show/77</ref>.
''Exemple :'' Ce bijou auricolore capte la lumière de manière étincelante.
=== Champ sémantique et usage ===
Le terme ''auricolore'' s’inscrit dans le champ des '''qualifications chromatiques et esthétiques'''. Il est particulièrement utile pour décrire avec précision les objets précieux, les bijoux, les tissus ou les éléments décoratifs qui possèdent une teinte dorée, là où le français courant recourt souvent à des expressions plus générales comme ''de couleur dorée'' ou ''doré''.
Une teinte ou un objet est dit ''auricolore'' lorsqu’il évoque la richesse, la chaleur et la luminosité de l’or.
=== Intérêt linguistique ===
''Auricolore'' présente un intérêt pour la langue française car il '''permet de condenser en un seul mot une nuance esthétique précise''', enrichissant le vocabulaire descriptif et poétique. Sa formation à partir du latin ''auricolor'' souligne à la fois la valeur stylistique et la précision lexicale de l'adjectif.
Ce mot est particulièrement adapté à la littérature, à la description artistique ou à tout contexte où '''la couleur et la brillance doivent être exprimées avec élégance et exactitude'''.
=== Étymologie ===
Du latin ''auricolor'' (« de couleur d’or »), francisé pour former un adjectif expressif et littéraire.
=== Source ===
[https://limiel.omeka.net/items/show/77 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 26 décembre 2025]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
fq0ki8kex6vugmjmk6r0chrgxl6gnqv
Guide des mots rares à adopter/blatérate
0
83756
763022
762054
2026-04-05T21:40:06Z
ROSEMARSH HOOD
122846
763022
wikitext
text/x-wiki
== blatérate ==
''Adjectif'' <small>(voir aussi [[Guide des mots rares à adopter/blatération|''blatération'']])</small>
'''Définition :'''
Se dit de quelqu’un ou de quelque chose qui '''ne mentionne rien de concret''', mais qui le donne à croire par son ton, son style ou son vocabulaire soigné. Par extension, ''blatérate'' qualifie une personne qui '''parle beaucoup pour ne rien dire''', multipliant les paroles creuses ou superficielles<ref>https://limiel.omeka.net/items/show/46</ref>.
''Exemple :'' La politicienne blatérate qu'était sa mère.
=== Champ sémantique et usage ===
Le terme ''blatérate'' s’inscrit dans le champ des '''paroles creuses, des discours superficiels ou des bavardages inutiles'''. Il permet de décrire avec précision un type de communication où le style peut tromper sur l’absence de contenu réel.
Un discours, un texte ou une personne est dite ''blatérate'' lorsqu’elle donne l’illusion de substance tout en restant vide de sens véritable.
=== Intérêt linguistique ===
''Blatérate'' enrichit la langue française en offrant '''un mot unique pour exprimer la vacuité du discours'''. Là où le français courant recourt à des périphrases comme ''parler pour ne rien dire'' ou ''discours creux'', ce néologisme permet de '''saisir à la fois la forme et le fond''' : le style trompeur et le manque de contenu.
Issu du latin ''blateratio'', il combine élégance et précision, et trouve sa place dans les écrits littéraires, critiques ou humoristiques pour qualifier avec justesse la prolixité vaine.
=== Étymologie ===
Du latin ''blateratio'' (« bavardage, paroles creuses »), francisé pour devenir un adjectif expressif et nuancé.
=== Source ===
[https://limiel.omeka.net/items/show/46 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 25 décembre 2025]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
s2k8rdwbp1ag3kwj00jkjhiw7a8wru1
Guide des mots rares à adopter/blatération
0
83757
763023
762055
2026-04-05T21:40:29Z
ROSEMARSH HOOD
122846
763023
wikitext
text/x-wiki
== blatération ==
''Nom féminin'' <small>(voir aussi ''[[Guide des mots rares à adopter/blatérate|blatérate]]'')</small>
'''Définition :'''
(''Ton satirique'') Désigne un '''bavardage frivole, superficiel''', une suite de paroles vaines ou légères, souvent répétitives et sans réel contenu<ref>https://limiel.omeka.net/items/show/45</ref>.
''Exemple :'' La blatération quotidienne des bourgeois rythmait les après‑midi du salon.
=== Champ sémantique et usage ===
Le terme ''blatération'' s’inscrit dans le champ des '''discours vides ou des bavardages futiles'''. Il est particulièrement adapté pour qualifier avec humour ou ironie des conversations où la forme prime sur le fond.
Une discussion, un récit ou une réunion peut être qualifiée de ''blatération'' lorsqu’elle se limite à des échanges superficiels, creux ou frivoles.
=== Intérêt linguistique ===
''Blatération'' enrichit la langue française en offrant '''un terme unique pour désigner le bavardage superficiel''', là où le français courant nécessite des périphrases comme ''paroles futiles'' ou ''bavardage vain''.
Issu du latin ''blateratio'', ce nom permet de conserver la nuance satirique et stylistique du mot, offrant une '''expression concise et élégante pour la critique sociale ou littéraire'''. Il est particulièrement utile en littérature, en critique sociale ou dans le registre humoristique pour décrire les excès de la parole vide.
=== Étymologie ===
Du latin ''blateratio'' (« bavardage, paroles creuses »), francisé pour devenir un nom expressif et nuancé.
=== Source ===
[https://limiel.omeka.net/items/show/45 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 25 décembre 2025]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
rg2ykr9k3mnsmhl483sxvgw1dbjov66
Guide des mots rares à adopter/burgenatif, burgenative
0
83758
763024
762056
2026-04-05T21:40:56Z
ROSEMARSH HOOD
122846
/* Étymologie */
763024
wikitext
text/x-wiki
== burgenatif, burgenative ==
''Nom et adjectif''
'''Prononciation :''' ''bur-gen-natif'' ou ''burgé-natif''
'''Définition (nom) :'''
# Personne née d'une '''famille riche ou bourgeoise''' ; par extension, quelqu’un qui '''vit dans une forteresse ou un château'''.
# Personne dont la '''réputation repose entièrement sur les actes ou la richesse de ses ancêtres'''. ''Exemple :'' Ces burgenatifs de la haute cité sont hautains envers les paysans.
'''Définition (adjectif) :'''
# Qualifie quelqu’un qui est né d’une famille riche ou bourgeoise ; par extension, vivant dans une forteresse ou un château.
# Qui tire sa réputation uniquement des actes ou de la richesse de ses ancêtres. ''Exemple :'' Les sieurs burgenatifs regardaient les nouveaux venus avec dédain<ref>https://limiel.omeka.net/items/show/51</ref>.
=== Champ sémantique et usage ===
Le terme ''burgenatif'' s’inscrit dans le champ des '''classes sociales, lignages et héritages de prestige'''. Il permet de décrire avec précision '''les personnes privilégiées par la naissance ou par l’héritage''', et peut être employé dans des contextes historiques, littéraires ou critiques.
Une personne est dite ''burgenative'' lorsqu’elle incarne '''l’aristocratie, le privilège familial ou le poids de la réputation ancestrale''', sans nécessairement avoir mérité son statut.
=== Intérêt linguistique ===
''Burgenatif'' est un mot rare d’intérêt pour la langue française, car il '''condense en un seul terme une idée complexe''' : la richesse héritée, le prestige familial et la réputation transmise par les ancêtres.
Sa formation originale résulte d’un '''télescopage lexical''' : le latin ''burgensis'' (habitant d’un bourg, bourgeois) combiné au français ''natif'', donnant un néologisme expressif, facilement mémorisable et utilisable à la fois comme nom et adjectif.
Ce mot enrichit le vocabulaire français en offrant '''une alternative concise et élégante''' pour évoquer les élites sociales et leur héritage, particulièrement utile en littérature, critique sociale ou écriture historique.
=== Étymologie ===
Du latin ''burgensis'' combiné morphologiquement au français ''natif'', francisé pour créer un terme descriptif et stylistique.
=== Source ===
[https://limiel.omeka.net/items/show/51 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 25 décembre 2025]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
7fafx9zak0znsvk5bb04bakxemp8gc9
Guide des mots rares à adopter/caullée
0
83759
763017
762058
2026-04-05T21:37:40Z
ROSEMARSH HOOD
122846
763017
wikitext
text/x-wiki
== caullée ==
''Nom féminin''
'''Définition :'''
# Ensemble de '''[[wikt:cavité|cavités]] ou de [[wikt:précipice|précipices]] [[wikt:contigu|contigus]]'''. ''Exemple :'' La caullée du Grand Canyon s’étendait à perte de vue.
# '''[[wikt:enceinte|Enceinte]] ou [[wikt:clôture|clôture]] d’un chef-d’œuvre architectural ancien'''. ''Exemple :'' La caullée du temple de la vallée protégeait les fresques sacrées<ref>https://limiel.omeka.net/items/show/107</ref>.
=== Champ sémantique et usage ===
Le terme ''caullée'' s’inscrit dans le champ des '''formations géographiques et structures architecturales'''. Il permet de désigner avec précision '''un ensemble de cavités naturelles''' ou '''une enceinte architecturale complexe''', évitant les périphrases longues comme ''ensemble de trous ou de précipices'' ou ''enceinte ancienne''.
Une formation géologique ou un ouvrage ancien peut être qualifié de ''caullée'' lorsqu’il présente un regroupement cohérent et contigu de cavités, de creux ou de murs protecteurs.
=== Intérêt linguistique ===
''Caullée'' est un mot rare qui enrichit la langue française en offrant '''un terme précis et élégant pour décrire des structures naturelles ou architecturales'''. Sa formation, dérivée du latin ''caullæ'', combine simplicité et expressivité, permettant aux auteurs, historiens ou géographes de '''décrire des paysages ou des architectures avec concision et style'''.
Ce nom commun est particulièrement utile dans les textes littéraires ou scientifiques où la précision et la nuance sont essentielles.
=== Étymologie ===
Du latin ''caullæ'', francisé pour devenir un nom descriptif et littéraire.
=== Source ===
[https://limiel.omeka.net/items/show/107 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 1er janvier 2026]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
6980s2vqx6pspdrv9fpo00mzzchmuxi
Fonctionnement d'un ordinateur/Les coprocesseurs : FPU et IO
0
83778
762997
762959
2026-04-05T17:56:10Z
Mewtow
31375
/* Les différents types de coprocesseurs */
762997
wikitext
text/x-wiki
Les processeurs actuels sont des processeurs multicœurs, à savoir qu'ils intègrent plusieurs processeurs dans un seul circuit intégré. Du moins, c'est une explication simplifiée, des circuits sont mutualisés entre les processeurs. Les cœurs sont tous identiques, à savoir qu'ils ont le même jeu d'instruction, ont une microarchitecture similaire, etc. Mais ce n'est possible que parce que les processeurs actuels ont un grand nombre de transistors, grâce à la loi de Moore. Pour intégrer plusieurs cœurs sur un processeur, il faut le budget en transistor pour. Mais au siècle dernier, le budget en transistors n'était pas là. Aussi, utiliser plusieurs processeurs était plus compliqué.
Les ordinateurs de l'époque pouvaient utiliser plusieurs processeurs identiques, mais il fallait que la carte mère le supporte. Et les cartes mères avec plusieurs sockets identiques étaient rares. Les cartes mères pour serveur en étaient capables, mais pas les cartes mères grand public. Les cartes mères plus grand public avaient cependant une option pour gérer plusieurs processeurs. Quelques ordinateurs assez anciens disposaient de '''coprocesseurs''', des processeurs secondaires qui complémentaient un processeur principal. Nous en avions déjà vu dans le chapitre sur l'architecture de base, et avions vu quelques exemples, provenant tous d'anciennes consoles de jeu. Mais il est temps de voir ces coprocesseurs en détail
==Les différents types de coprocesseurs : son, IO, FPU==
Le CPU et le coprocesseur sont foncièrement différents : ils n'ont pas le même jeu d'instruction, n'ont pas le même rôle, et j'en passe. Sauf exception, le CPU et le coprocesseur exécutent des programmes différents, ils travaillent en parallèle. Le coprocesseur a donc un rôle secondaire, reste à voir à quoi il sert.
===Les différents types de coprocesseurs===
Les coprocesseurs peuvent se classer en plusieurs catégories : les coprocesseurs sonores, arithmétiques, et d'entrées-sorties.
* Les '''coprocesseurs sonores''' sont une sorte d'ancêtre des cartes son, utilisés autrefois sur les consoles de jeu.
* Les '''coprocesseurs d'entrée-sortie''' ou ''Channel IO'', délèguent la gestion des entrée-sorties à un coprocesseur.
* Les '''coprocesseurs arithmétiques''' sont spécialisés dans les calculs, et notamment dans les calculs flottants.
Les '''coprocesseurs sonores''' ont eu leur heure de gloire sur les anciennes consoles de jeux vidéo, comme La Nintendo 64, la Playstation et autres consoles antérieures. Ils s'occupaient respectivement de calculer tout ce qui a trait au son. Pour donner un exemple, on peut citer la console Neo-géo, qui disposait de deux processeurs travaillant en parallèle : un processeur principal, et un coprocesseur sonore. Le processeur principal était un Motorola 68000, alors que le coprocesseur sonore était un processeur Z80.
Les '''coprocesseur d'IO''' sont dédiés à l'accès aux entrées-sorties. Ils prennent en charge des transferts DMA et on peut les voir comme des controleurs DMA devenus programmables. Nous en avions parlé dans le chapitre sur les entrée-sorties, je ne reviendrais pas dessus ici.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les '''coprocesseurs arithmétiques''' sont dédiés aux calculs en virgule flottante. Ils étaient utilisés à une époque où les CPU ne géraient que des calculs entiers (en binaire ou en BCD). Un exemple est le coprocesseur flottant x87, complémentaire des premiers processeurs Intel x86. Il y a eu la même chose sur les processeurs Motorola 68000, avec deux coprocesseurs flottants appelés les Motorola 68881 et les Motorola 68882. Ils sont aujourd'hui tombés en désuétude, depuis que les CPU sont devenus capables de faire des calculs sur des nombres flottants.
===Les coprocesseurs faiblement et fortement couplés===
Un coprocesseur peut être un processeur fait sur mesure, qui ne peut que servir de processeur secondaire, comme le sont tous les coprocesseurs arithmétiques. À l'inverse, de nombreuses consoles de jeu utilisaient un processeur des plus banals comme coprocesseur. Par exemple, de nombreuses consoles avaient un processeur principal, complété par un coprocesseur Z80 en guise de carte son. Et cette distinction est très liée à celle qui distingue les coprocesseurs faiblement couplés, et fortement couplés.
Le cas le plus fréquent est celui où le CPU et le coprocesseur travaillent en parallèle et exécutent des programmes différents. On parle alors de '''coprocesseurs faiblement couplés'''. Les coprocesseurs sonores et d'IO sont tous dans ce cas. Le coprocesseur sonore exécute un programme pour gérer le son, qui est séparé au programme principal. Le programme principal communique avec le coprocesseur, mais c'est assez rare. Dans un jeu vidéo, cela arrive seulement quand il faut changer de musique ou déclencher un effet sonore.
Les deux travaillent en parallèle, et il existe des mécanismes de synchronisation entre les deux processeurs. Typiquement, la synchronisation se fait en utilisant des ''interruptions inter-processeurs'', à savoir qu'un processeur exécute une interruption qui est exécutée par l'autre processeur. L'implémentation demande d'ajouter des circuits pour gérer ces interruptions entre processeurs.
Un exemple est celui des consoles néo-géo et Megadrive. Elles intègrent deux processeurs : un Motorola 68000 qui sert de processeur principal, un Z80 qui sert de processeur dédié à l'audio. Le MC68000 envoie des commandes au Z80, mais la communication ne va pas dans l'autre sens. Les deux processeurs communiquent via l'intermédiaire d'un ''IO arbiter chip'', qui gère les interruptions inter-processeur. Il contient un registre de 8 bits, dans lequel le MC68000 peut écrire un numéro d'interruption, qui indiquent quelle routine d'interruption exécuter. Lorsque le MC68000 écrit une valeur dedans, cela déclenche l’exécution automatique d'une interruption sur le Z80.
[[File:Architecture de la Megadrive et de la Néogeo.png|centre|vignette|upright=2.5|Architecture de la Megadrive et de la Néogeo]]
Les coprocesseurs arithmétiques se distinguent des autres, car ils fonctionnent en tandem avec le processeur principal, pas en parallèle. Les coprocesseurs précédents sont autonomes, à savoir qu'ils exécutent un programme différent de celui exécuté par le CPU. Mais les coprocesseurs arithmétiques ne sont pas dans ce cas. Il n'y a qu'un seul programme à exécuter, qui contient des instructions à destination du CPU, d'autres à destination du coprocesseur. Les instructions sont exécutées soit par le CPU, soit par le coprocesseur, une par une. En clair, le CPU et le coprocesseur se passent à la main à tour de rôle, ils ne travaillent pas en parallèle. On parle alors de '''coprocesseurs fortement couplés'''.
==Les coprocesseurs arithmétiques : généralités et exemples==
Dans le reste de ce chapitre, nous allons surtout voir les coprocesseurs arithmétiques, et ce pour plusieurs raisons. Premièrement, on a plus de documentation dessus, car c'était des processeurs commerciaux, vendus autrefois en magasin, avec de la documentation destinée aux utilisateurs. À l'opposé, les coprocesseurs sonores et d'IO étaient surtout utilisées sur des consoles de jeu, ou des gros ''mainframes''. Et autant les ''mainframes'' avaient de la documentation bien fournie, autant celle pour les consoles de jeu était très limitée. Une autre raison est que les autres coprocesseurs devraient être vus idéalement dans les chapitres sur les périphériques et entrées-sorties. C'est assez évident pour les coprocesseurs d'IO, mais c'est aussi l'idéal pour les coprocesseurs sonores. En effet, ceux-ci sont en quelque sorte des cartes sons partielles.
===Les anciens co-processeurs flottants des PCs===
Les coprocesseurs arithmétiques étaient spécialisés dans les calculs flottants et étaient généralement fortement couplés. Les coprocesseurs étaient optionnels et il était parfaitement possible de monter un PC qui n'en avait pas. En conséquence, les programmeurs devaient coder des programmes qui peuvent fonctionner avec et sans coprocesseur. La solution la plus simple était de fournir deux versions du logiciel : une sans usage du coprocesseur, et une autre qui en fait usage, plus rapide. Une autre solution était d'émuler les calculs flottants en logiciel.
Un bon exemple de la seconde solution est le 68881 de Motorola, conçu pour fonctionner avec les CPU 6802 et 68030. Les programmes mixaient instructions entières et flottantes, le 68000 exécutant les instructions entières, le 68881 exécutant les instructions flottantes. Les instructions flottantes avaient un opcode qui commençait par F (en hexadécimal), ce qui permettait de les distinguer rapidement du reste. Le 68000 chargeait les instructions, et regardait si l'instruction était destinée soit au coprocesseur, soit pour lui. Si elle était pour le coprocesseur, le 68000 lisait les opérandes depuis la mémoire et envoyait tout au coprocesseur 68881, qui exécutait l'instruction, puis rendait la main au 68000.
Les coprocesseurs Motorola utilisaient des flottants codés sur 80 bits : la mantisse était codée sur 64 bits, l'exposant sur 15 bits. Le 68881 incorporait 8 registres flottants, nommés, de 80 bits chacun. Il avait aussi un registre de statut et un registre de contrôle, guère plus. Fait étonnant, les coprocesseurs pour les PC faisaient pareil : flottants codés sur 80 bits, 8 registres flottants. Mais les détails sont différents, comme on le verra plus tard.
===Les coprocesseurs arithmétiques des consoles de jeu===
Les consoles de jeu incorporent souvent des coprocesseurs. Mais il est assez rare qu'elles incorporent des coprocesseurs arithmétiques et c'est surtout quelque chose qu'on observe sur les consoles des années 2000. La raison est que les consoles utilisent des processeurs commerciaux, utilisés dans les smartphones ou les PC. Elles n'utilisent pas de processeur spécifiquement conçu pour elles, sauf exceptions. Et de tels processeurs disposent de capacités de calcul flottant ou entières suffisantes, ils sont choisis pour. Mais il existe quelques cas où ce n'est pas le cas.
Un exemple récent est de la console de jeu Nintendo DS. La console utilisait deux processeurs, un ARM9 et un ARM7, deux processeurs RISC qui ne pouvaient pas faire de division entière. Il s'agit pourtant d'opérations importantes dans le cas du rendu 3D, ce qui fait que les concepteurs de la console ont rajouté un coprocesseur spécialisé dans les divisions entières et les racines carrées. Le coprocesseur était adressable directement par le processeur, comme peuvent l'être la RAM ou les périphériques, et était traité comme une entrée-sortie comme une autre.
Un autre exemple est celui du processeur CELL de la console de jeu PS3. Il était conçu spécifiquement pour cette console. Il intègre un cœur principal POWER PC v5 et 8 cœurs qui servent de processeurs auxiliaires. Le processeur principal est appelé le PPE et les processeurs auxiliaires sont les SPE. Les SPE sont reliés à une mémoire locale (''local store'') de 256 kibioctets qui communique avec le processeur principal via un bus spécial. Cette fois-ci, les coprocesseurs sont intégrés dans le même processeur.
Les SPE communiquent avec la RAM principale via des contrôleurs DMA. Les SPE possèdent des instructions permettant de commander leur contrôleur DMA et c'est le seul moyen qu'ils ont pour récupérer des informations depuis la mémoire. Et c'est au programmeur de gérer tout ça ! C'est le processeur principal qui va envoyer aux SPE les programmes qu'ils doivent exécuter. Il délègue des calculs aux SPE en écrivant dans le local store du SPE et en lui ordonnant l’exécution du programme qu'il vient d'écrire.
[[File:Schema Cell.png|centre|vignette|upright=2|Architecture du processeur CELL de la PS3. Le PPE est le processeur principal, les SPE sont des processeurs auxiliaires qui comprennent : un ''local store'' noté LS, un processeur noté SXU, et un contrôleur DMA pour échanger des informations avec la mémoire principale.]]
==Le jeu d'instruction x87 d'Intel==
Un exemple de coprocesseur arithmétique est celui de l'extension x87 qui a ajouté les nombres flottants aux processeurs x86. Pour rappel, les processeurs x86 sont ceux qui sont présents à l'intérieur des PC, à savoir tous les processeurs Intel et AMD actuels. Il s'agit d'un jeu d'instruction qui a reçu de nombreux ajouts au cours du temps. Les ajouts en question sont appelés des '''extensions x86'''. Nous allons maintenant voir celle qui a ajouté la gestion des flottants au x86. L'extension x87 n'est plus utilisée depuis l'arrivée des CPU 64 bits, car elle a été remplacée par l'extension SSE.
Sur les tout premiers processeurs x86, le support des nombres flottants n'était pas implémenté. A la place, ils utilisaient des coprocesseurs arithmétiques, appelés des '''coprocesseurs x87'''. Ils travaillaient en tandem avec un processeur x86 normal, et ne géraient que des instructions arithmétiques sur des flottants. Ils étaient capables d'exécuter les 4 opérations de base (add, sub, mul, div), la racine carrée, les opérations trigonométriques sinus, cosinus et tangente, l'arc tangente, et des instructions de calcul de logarithmes ou d'exponentielles.
===La pseudo-pile de registres===
Les coprocesseurs arithmétiques x87 avaient 8 registres flottants, qui étaient gérés avec une pseudo-pile, ainsi que 3 registres de contrôle. Les 8 registres x87 sont ordonnés et numérotés de 0 à 7. Les registres x87 sont organisés sous la forme d'une pile de registres, similaire à une pile d'assiette, que l'on remplit dans un ordre bien précis. Lors du chargement d'un opérande de la RAM vers les registres, il n'est pas possible de décider du registre de destination. Si on veut ajouter des flottants dans nos registres, on doit les « remplir » dans un ordre de remplissage imposé : on remplit d'abord le registre 7, puis le 6, puis le 5, et ainsi de suite jusqu'au registre 0. Si on veut ajouter un flottant dans cette pile de registres, celui-ci sera stocké dans le premier registre vide dans l'ordre de remplissage indiqué au-dessus.
Prenons un exemple, les 3 premiers registres sont occupés par un flottant et on veut charger un flottant supplémentaire : le 4e registre sera utilisé pour stocker ce flottant. La même chose existe pour le « déremplissage » des registres. Imaginez que vous souhaitez déplacer le contenu d'un registre dans la mémoire RAM et effacer complètement son contenu. On ne peut pas choisir n'importe quel registre pour faire cela : on est obligé de prendre le registre non vide ayant le numéro le plus grand.
{|
|[[File:Pseudo-pile x87. - PUSH.png|vignette|upright=2|Pseudo-pile x87 - chargement d'une opérande.]]
|[[File:Pseudo-pile x87 - POP.png|vignette|upright=2|Pseudo-pile x87 - retrait d'une opérande.]]
|}
Les instructions à un opérande dépilent le flottant au sommet de la pile. Les instructions dyadiques peuvent dépiler les deux opérandes au sommet de la pile, mais elles peuvent aussi utiliser d'autres modes d'adressage. Elles peuvent aller chercher la seconde opérande en RAM, en fournissant une adresse. Mais elles peuvent aussi adresser n'importe quel autre registre de la pile en fournissant son numéro de registre. Avec ce dernier mode d'adressage, le processeur agit comme une sorte de processeur à accumulateur, avec le sommet de la pile servant d'accumulateur.
En somme, les registres de la pile sont adressables, du moins pour ce qui est de gérer la seconde opérande. C'est cette particularité qui vaut le nom de pseudo-pile à cette organisation à mi-chemin entre une pile et une architecture à registres.
===Les instructions x87===
Maintenant, voyons quelles instructions les FPU x87 devaient gérer. L'extension x87 est en effet un jeu d'instruction, qui décrit quelles instructions doivent être gérées. Les instructions utilisent des opcodes inutilisés dans le jeu d'instruction x86, qui sont détournés pour fonctionner sur le x87.
On trouve évidemment des instructions de calculs, bien évidemment compatibles avec la norme IEE754 : l'addition FADD, la soustraction FSUB, la multiplication FMUL et la division FDIV. Mais il y a aussi la racine carrée FSQRT. La FPU x87 implémente aussi des instructions trigonométriques, qui ne sont pas supportées par la norme IEEE 754. Elle gérait l'instruction FCOS pour le cosinus, l'instruction FSIN pour le sinus, l'instruction FPTAN pour la tangente, l'instruction FPATAN pour l'arc tangente, ainsi que des instructions de calcul de logarithmes ou d'exponentielles.
La FPU x87 dispose aussi d'instructions de comparaisons compatibles avec la norme IEEE 754, capables de comparer le flottant au sommet de la pile avec un autre nombre qui peut être flottant ou entier ! Voici une liste de quelques instructions de comparaisons supportées par les FPU 87 :
* FCOM : compare le contenu du registre 0 avec une constante flottante ;
* FCOMI : compare le contenu des registres 0 et 1 ;
* FICOM : compare le contenu du registre 0 avec une constante entière ;
* FTST : compare le registre numéroté 0 avec la valeur 0.
Le x87 avait aussi des instructions de calcul de la valeur absolue (FABS) ou encore de changement de signe (FCHS).
Les instructions de calcul n'ayant besoin que d'un seul flottant pour s'exécuter, comme les opérations trigonométriques ou la valeur absolue, utilisent le flottant situé au sommet de la pile. Les instructions dyadiques (multiplication, addition, soustraction et autres) vont agir différemment suivant la situation. Elles peuvent prendre les deux flottants les plus haut placés dans cette pile, prendre le flottant au sommet de la pile, utiliser une donnée en provenance de la mémoire, ou encore utiliser le flottant le plus haut placé et un flottant stocké dans l'importe quel registre de cette pile de registres. La pile de registre était donc une sorte de mélange entre un accumulateur et 7 registres adressables.
Pour charger des opérandes dans la pile d'opérande, l'extension x87 fournit trois instructions d'accès mémoire.
{|class="wikitable"
|-
! Instruction
! Description
|-
! FLD
| Charge un nombre flottant depuis la mémoire vers notre pile de registres vue au-dessus. Cette instruction peut charger un flottant codé sur 32 bits, 64 bits ou 80 bits
|-
! FSTP
| Déplace le contenu d'un registre vers la mémoire. Une autre instruction existe qui est capable de copier le contenu d'un registre vers la mémoire sans effacer le contenu du registre : c'est l'instruction FST
|-
! FXCH
| Échange le contenu du dernier registre non vide dans l'ordre de remplissage (celui situé au sommet de la pile) avec un autre registre
|}
D'autres instructions existent qui chargent certaines constantes (PI, 1, 0, certains logarithmes en base 2) dans le registre au sommet de la pile de registres.
===Les registres de contrôle et d'état===
Pour gérer la pseudo-pile, les registres pour les flottants sont associés à un registre d'état nommé '''''Tag Word'''''. Le registre ''Tag Word'' indique, pour chaque registre flottant, s'il est vide ou non. Avouez que c'est pratique pour gérer la pile de registres vue au-dessus ! Le registre ''tag word'' fait 16 bits, ce qui fait 2 bits pour chacun des 8 registres. Ces deux bits contiennent des informations sur le contenu du registre de données réservé.
* Si ces deux bits valent 00, le registre contient un flottant « normal » différent de zéro ;
* Si ces deux bits valent 01, le registre contient une valeur nulle : 0 ;
* Si ces deux bits valent 10, le registre contient un NAN, un infini, ou un dénormal ;
* Si ces deux bits valent 11, le registre est vide et ne contient pas de nombre flottant.
Le processeur x87 contient aussi deux registres d'état, nommés ''Control Word'' et ''Status Word''. Le registre ''Status Word'' contient quelques bits, certains utilisés pour gérer la pseudo-pile, d'autres non. Il fait lui aussi 16 bits et c'est un registre d'état qui est utilisé pour qu'un programme puisse comprendre la cause d'une exception. Il contient le numéro du registre juste au-dessus du sommet de la pile, le numéro du premier registre vide dans l'ordre de remplissage. Mais il contient surtout des bits mis à 1 en cas de débordement de flottant, de division par zéro, lorsqu'un calcul a pour résultat un dénormal, etc.
{|class="wikitable"
|-
! Bit !! Utilité
|-
! TOP
| Trois bits, qui codent le numéro du premier registre vide dans la pile de registre
|-
! U
| Détecte les ''underflows'' : est mis à 1 en cas d'''underflows''.
|-
! O
| | Détecte les ''overflows'' : est mis à 1 en cas d'''overflows''.
|-
! Z
| Prévient qu'une division par zéro a eu lieu. Est mis à 1 si c'est le cas.
|-
! D
| Bit est mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal
|-
! I
| Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0
|}
Le registre ''Control Word'' fait 16 bits et configure la gestion des arrondis.
{|class="wikitable"
|-
! Bit !! Utilité
|-
! Infinity Control
| S'il vaut zéro, les infinis sont tous traités comme s'ils valaient +∞. S'il vaut un, les infinis sont traités normalement
|-
! Rouding Control
| C'est un ensemble de deux bits qui détermine le mode d'arrondi utilisé
* 00 : vers le nombre flottant le plus proche : c'est la valeur par défaut ;
* 01 : vers - l'infini ;
* 10 : vers + l'infini ;
* 11 : vers zéro
|-
! Precision Control
|`Ensemble de deux bits qui détermine la taille de la mantisse de l'arrondi du résultat d'un calcul. En effet, on peut demander à notre FPU d'arrondir le résultat de chaque calcul qu'elle effectue. Cette instruction ne touche pas à l'exposant, mais seulement à la mantisse. La valeur par défaut de ces deux bits est 11 : notre FPU utilise donc des flottants double précision étendue. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754.
* 00 : mantisse codée sur 24 bits ;
* 01 : valeur inutilisée ;
* 10 : mantisse codée sur 53 bits ;
* 11 : mantisse codée sur 64 bits
|}
Les instructions x87 sont codées sur au minimum deux octets. Le premier octet commence toujours la suite de bit 11011, qui indique que c'est une instruction destinée au coprocesseur. Le 11011 était appelé le code d'échappement, sa mnémonique en assembleur était ESC. Le tout est suivi par 6 bits d'opcode, et 5 bits pour le mode d'adressage. Le tout était regroupé comme suit : 1101 1xxx aaxx xaaa, avec x les bits de l'opcode et a pour le mode d'adressage. Le tout était suivi par des opérandes, selon le mode d'adressage.
==Les coprocesseurs x87 et leurs précurseurs==
Les premiers coprocesseurs de ce type étaient l'Intel 8231/8232, destinés à être utilisés avec le 8088, un processeur 8 bits. Par la suite, Intel a récidivé avec le 8087, qui était destiné pour servir en tandem avec le 8086. Il a été suivi par les Intel 187, le 287, le 387, le 487 et le 587, qui étaient censés servir avec les CPU 186, 286, 386, 486, etc. Mais d'autres compagnies ont crée des coprocesseurs x87, comme Weitek, Cyrix, AMD, Texas Instrument, et bien d'autres.
===Les précurseurs : l'Intel 8231 et le 8232===
L'Am9511 et Am9512 sont une des toutes premières FPU pour PC, si ce n'est les premières si on en croit AMD. Ils ont été licenciés par Intel sous le nom d'Intel 8231 et le 8232. Ils étaient conçus pour complémenter le CPU Intel 8080, mais on pouvait parfaitement les utiliser avec d'autres CPU, comme le Z80. La [http://ep.homeserver.hu/PDF/AM9511A-9512.pdf documentation AMD donnait même des exemples assez variés]. La raison est qu'on y accédait comme n'importe quelle entrée-sortie connectée au bus système. Ils étaient accessibles via ''pooling'', interruptions ou même via DMA. Nous expliquerons comment c'est possible plus bas.
L'Intel 8231 ne supportait pas le jeu d'extension x87, qui est apparu après. Il gérait des nombres flottants de 32 bits, mais aussi des nombres en virgule fixe de 16 et 32 bits. Les flottants 32 bits suivaient globalement la norme IEEE 754, mais les nombres en virgule fixe utilisaient un format propriétaire. Il gérait les quatre opérations de base, mais aussi des calculs trigonométriques. Il utilisait pour cela du microcode, avec une approximation basée sur des polynômes de Tchebychev. L'intel 8232 supportait lui des flottants 32 et 64 bits, mais ne supportait que les quatre opérations de base (addition, soustraction, multiplication et division).
: La documentation décrit ces flottants 32 bits comme étant de la double précision, mais c'est parce que la terminologie de l'époque n'était pas encore bien stabilisée.
[[File:C8231A FPU PIN CONFIGURATION.png|thumb|Intel 8231 FPU PIN CONFIGURATION.]]
Les broches de l'intel 8231 sont illustrées ci-contre. La plupart des broches nous sont familières : 8 broches pour le bus de données (qui fait 8 bits), une entrée d'horloge, une entrée de RESET, une entrée ''chip select'' pour le décodage d'adresse. Les broches restantes sont très intéressantes, mais on les verra dans ce qui suit.
Toujours est-il que le coprocesseur est relié à un bus de 8 bits, alors que ses registres font 32 à 64 bits. Pour cela, le 8231/8232 lisait les opérandes octet par octet depuis le bus de données. Idem mais pour les écritures. Par contre, les instructions sont prévues pour faire 8 bits, pas plus. Pour avoir des instructions aussi courtes, la seule solution est d'utiliser une machine à pile et c'est ce que le 8231/8232 a fait. Précisons cependant que ce n'est pas la même pile de registre que la pile x87, mais c'était une sorte de pile similaire, qui a évolué pour donner la pile x87. Il s'agissait pour le coup d'une vraie pile, les opérations utilisaient systématiquement le sommet de la pile et l'opérande en dessous. Il n'y avait pas de possibilité d'adresser un opérande dans la pile.
Le processeur intégrait 8 registres de 16 bits, organisés comme une pile. Les registres pouvaient être utilisés : soit comme une pile de 8 opérandes 16 bits, soit une pile de 4 opérandes 32 bits, soit une pile de 2 opérandes 64 bits. Les opérandes étaient empilées octet par octet dans le processeur. Ils étaient dépilés là aussi octet par octet. Pour cela, le 8231/8232 dispose de trois entrées nommées A0, RD et WR. Les trois bits décident s'il faut faire une lecture, une écriture, exécuter une instruction, ou lire le registre d'état. Les quatre opérations sont appelées des commandes dans la documentation Intel et AMD. Les trois entrées font donc office de bus de commande simplifié.
{|class="wikitable"
|-
! A0, RD, WR !! Action
|-
| 000 || Lecture de l'octet depuis les registres de données.
|-
| 010 || Écriture de l'octet dans les registres de données.
|-
| 111 || L'octet est l'opcode d'une instruction, qui est exécutée immédiatement.
|-
| 101 || Lecture du registre d'état.
|}
Le processeur principal envoyait des commandes à l'Intel 8231/8232, qui les exécutait dans son coin. Le 8231/8232 envoyait un signal END OF EXECUTION pour prévenir qu'il avait fini son travail, que la commande précédente était terminée. Il avait une broche dédiée, appelée END, dédiée à ça. Le coprocesseur avait donc une interface de communication asynchrone, qui se voit quand on étudie ses broches. Les broches suivantes servent à la communication asynchrone avec le 8231/8232.
* READY est à 1 quand le 8231/8232 est libre, capable d'accepter une nouvelle instruction/commande. Il passe à 0 quand une instruction démarre, avec la commande 111 vue plus haut.
* END indique que la commande précédente a terminé son exécution. Lorsque END passe à 1, BUSY passe automatiquement à 0.
* EACK est une entrée sur laquelle le processeur dit qu'il a bien reçu le signal END, et que ce dernier peut être remis à 0.
Ce système pouvait être utilisé avec du ''pooling'', avec des interruptions, voire du DMA. Avec des interruptions, la sortie END était utilisée comme sortie d'interruption, reliée au CPU ou au contrôleur d'interruption. Pour le ''pooling'', le registre d'état du 8231/8232 contenait un bit BUSY, qui indiquait si le coprocesseur était utilisé ou non.
Un tel fonctionnement peut sembler étrange, et vous aurez l'impression que communiquer avec le coprocesseur est très lent. Mais cela prend tout son sens quand on connait le temps mis pour exécuter une instruction sur le coprocesseur. Une opération simple sur des flottants 32 bits prenait facilement une cinquantaine de cycles d'horloge, et c'était parmi les meilleurs temps de calcul. Il n'était pas rare d'avoir des opérations prenant plusieurs centaines, voire milliers de cycles d'horloge. Pas loin de 5000 cycles d'horloge pour une division de deux flottants 64 bits sur le 8232, plusieurs dizaines de milliers de cycles pour certaines opérations trigonométriques. Et le pire, c'était que c'était plus rapide que l'émulation logicielle !
Pas étonnant donc que le 8231/8232 aient été traités comme des entrées-sorties, à une époque ou tout était connecté sur un bus système assez rapide. Un autre avantage est que le 8231/8232 pouvaient fonctionner à une fréquence sans rapport avec celle du processeur. Par exemple, on pouvait utiliser un processeur à 1 MHz alors que le 8231/8232 allait à 4 MHz. Le coprocesseur faisait juste des calculs rapidement, comparé au CPU. Et ça a été utilisé sur certains systèmes Apple II. Ou encore, on pouvait utiliser un processeur légèrement plus rapide que le coprocesseur, avec quelques MHz de différence, comme un CPU à 5 MHz avec un coprocesseur de 2 MHz.
===L'intel 8087 et ses successeurs===
[[File:Intel 8087.svg|vignette|Intel 8087]]
Le 8087 été fabriqué avec 65 000 transistors. Le 8087 avait pour particularité qu'il était connecté directement sur le bus mémoire, au même titre que le 8086. Mais le 8087 n'avait pas de bus d'adresse et de données séparé. Le processeur utilisait un bus multiplexé. Il avait 20 broches pour se connecter au bus : 16 d'entre elles servaient alternativement de bus d'adresse et de données. L'interface avec le bus était donc un peu compliquée.
L'intel 387 était le coprocesseur associé au 386 d'Intel. Il était le premier coprocesseur à s'intégrer sur un bus de 32 bits. Il a été décliné en plusieurs versions, dont certaines sont spécifiques à un modèle de 386. Par exemple, le i386SX était une version simplifiée du 386 initial, qui avait notamment un bus de seulement 16 bits. Et de ce fait, il avait son propre coprocesseur i387SX, qui était adapté à un bus de 16 bits. De même, le i386SL était adapté aux ordinateurs portables et avait son propre coprocesseur i387SL.
Tout ce qui va suivre est valide pour tous les coprocesseurs x87 de marque Intel.
Le processeur central lisait des instructions, en envoyant le ''program counter'' sur le bus d'adresse, les instructions étaient récupérées sur le bus de données. Là, les deux processeurs déterminaient si l'instruction chargée était destinée au coprocesseur ou au CPU. Pour cela, les instructions x87 commencent toutes par la suite de bit 11011, qui permet de savoir facilement si une instruction est destinée au coprocesseur. Le 11011 était suivi par un opcode et un mode d'adressage.
Si le mode d'adressage demandait de lire un opérande mémoire, le 8086 envoyait l'adresse de l'opérande sur le bus, et le coprocesseur récupérait celle-ci sur le bus de données. Si l'opérande devait être lu en plusieurs fois, le coprocesseur lisait le reste de lui-même, en prenant le contrôle du bus d'adresse. Il récupérait l'adresse envoyée initialement par le CPU, puis l'incrémentait et relançait un nouvel accès mémoire. Il l'incrémentait autant de fois que nécessaire pour charger l'opérande.
Un problème est que le CPU ne sait pas combien de temps dure une instruction x87. Et cette durée dépendait de l'implémentation du processeur, elle n'était pas la même selon la marque du coprocesseur. Un 186 n'avait pas les mêmes timings que le 286, par exemple. Pour le CPU, une instruction x87 met juste deux cycles pour s'exécuter (plus si des opérandes doivent être lus en mémoire). Pour cela, le CPU disposait d'un mécanisme de synchronisation.
Le mécanisme de synchronisation était une instruction WAIT, qui forçait le CPU à attendre que le coprocesseur ait terminé l'instruction précédente. L'implémentation matérielle était assez simple. Le coprocesseur disposait d'une sortie BUSY, qui indiquait qu'il était en train d'exécuter une instruction et ne pouvait pas en accepter une nouvelle. Le CPU, quant à lui, avait une entrée TEST qui vérifiait si le, coprocesseur était occupé ou non. La sortie BUSY était reliée à l'entrée TEST. L'instruction test vérifiait juste ce qu'il y avait sur l'entrée TEST. Tant qu'elle était à 1, le processeur attendait et ne chargeait pas de nouvelle instruction. Dès qu'elle passe à 0, l'exécution reprend.
Il faut noter que l'instruction WAIT n'est nécessaire qu'entre deux instructions flottantes assez proches. Mais il est possible d'intercaler des instructions entières entre deux instructions flottantes. Le programme pouvait ainsi mixer instructions entières et flottantes, les instructions entières étant exécutées sur le 8086, les instructions flottantes sur le coprocesseur. Il y avait donc une possibilité de parallélisme, à savoir que les deux processeurs pouvaient exécuter des instructions différentes en même temps. Mais cela demandait que les calculs soient coopératifs et mélangent bien entiers et flottants.
Le 8087 et ses successeurs avaient une microarchitecture assez simple. L'unité de contrôle contenait un décodeur d'instruction microcodé, le registre de contrôle, le registre d'état. La plupart des instructions sont microcodées, l'unité de calcul est assez limitée. Elle permet d'additionner deux mantisses flottantes, de faire des décalages, d'additionner deux exposants, mais pas plus. Les multiplications et divisions sont donc microcodées et émulées en enchainant des additions flottantes. Les instructions trigonométriques sont implémentées en utilisant l'algorithme CORDIC, qu'on a vu dans le chapitre sur les circuits de calcul flottant.
[[File:Intel 8087 arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel 8087.]]
Le chemin de données est composé d'un banc de registre flottant pour la pseudo-pile, et de plusieurs circuits de calcul. Le banc de registre était mono-port, ce qui fait que les ALUs étaient précédés par deux registres temporaires pour les opérandes. Les circuits pour l'exposant et la mantisse sont séparés, et sont même reliés au banc de registre par deux bus séparés. Il y a un additionneur pour les exposants, un additionneur pour les mantisses et un décaleur pour les mantisses (pour les normaliser).
De plus, on trouve une mémoire ROM dédiée aux constantes les plus utilisées. Elle sert pour les constantes de base, gérées par le jeu d'extension x87. Mais elle contient aussi des constantes utilisées pour l'algorithme CORDIC. Elle n'est pas illustrée sur le schéma ci-dessous, mais elle existe.
L'interface avec le bus est un simple registre d’interfaçage avec le bus. Pour rappel, le bus de données fait 16 bits sur le 8087, 32 bits sur le 387. Entre le bus et le chemin de données, on trouve une file servant à simplifier la gestion des lectures. L'idée est que les opérandes lus/écrits font 32, 64 ou 80 bits, alors que le bus de données n'en fait que 16/32. Les opérandes sont donc lus/écrits en plusieurs passes. Sur le 8087, il doit réaliser deux passes pour des opérandes de 32 bits, quatre passes pour celles de 64 bits, 5 pour des opérandes de 80 bits (80 = 5 × 16). Le 387 doit faire deux fois moins.
[[File:Intel 387 arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel 387. Les circuits pour les exposants sont à gauche dans le chemin de données, les circuits pour les mantisses sont à droite.]]
L'implémentation du banc de registre est assez simple : une RAM avec un registre qui indique la position du sommet de la pile dedans. Le registre fait 3 bits, pour 8 registres. En plus de cela, il y a un petit soustracteur et un multiplexeur, pour adresser les opérandes dans la pile. Pour rappel, il est possible d'adresser la seconde opérande dans la pile. Mais on précise pas le numéro du registre dans la pile pour cela, on précise sa position sous le sommet de la pile, à savoir si elle est deux, trois, quatre opérandes sous le sommet de la pile. Pour déterminer quel registre lire, il faut soustraire ce "décalage" au numéro de registre du sommet de la pile. Pour cela, il y a un petit soustracteur pour faire le calcul.
Le circuit décaleur est composé de deux sous-décaleurs. Le premier fait des décalages au niveau des octets, le second décale l'opérande de 0 à 7 rangs.
Pour finir, voici quelques liens sur la microarchitecture du 8087 :
* [https://www.righto.com/2026/02/8087-instruction-decoding.html Instruction decoding in the Intel 8087 floating-point chip].
* [https://www.righto.com/2020/05/die-analysis-of-8087-math-coprocessors.html Die analysis of the 8087 math coprocessor's fast bit shifter].
* [https://www.righto.com/2025/12/8087-stack-circuitry.html The stack circuitry of the Intel 8087 floating point chip, reverse-engineered].
* [https://www.righto.com/2025/12/8087-microcode-conditions.html Conditions in the Intel 8087 floating-point chip's microcode].
* [https://www.righto.com/2020/05/extracting-rom-constants-from-8087-math.html Extracting ROM constants from the 8087 math coprocessor's die].
* [https://www.righto.com/2018/09/two-bits-per-transistor-high-density.html Two bits per transistor: high-density ROM in Intel's 8087 floating point chip].
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les sections critiques et le modèle mémoire
| prevText=Les sections critiques et le modèle mémoire
| next=L'accélération matérielle de la virtualisation
| nextText=L'accélération matérielle de la virtualisation
}}
</noinclude>
9s89vfxzocdflwjqteezrwr251bcdq3
763006
762997
2026-04-05T18:33:01Z
Mewtow
31375
/* Les différents types de coprocesseurs */
763006
wikitext
text/x-wiki
Les processeurs actuels sont des processeurs multicœurs, à savoir qu'ils intègrent plusieurs processeurs dans un seul circuit intégré. Du moins, c'est une explication simplifiée, des circuits sont mutualisés entre les processeurs. Les cœurs sont tous identiques, à savoir qu'ils ont le même jeu d'instruction, ont une microarchitecture similaire, etc. Mais ce n'est possible que parce que les processeurs actuels ont un grand nombre de transistors, grâce à la loi de Moore. Pour intégrer plusieurs cœurs sur un processeur, il faut le budget en transistor pour. Mais au siècle dernier, le budget en transistors n'était pas là. Aussi, utiliser plusieurs processeurs était plus compliqué.
Les ordinateurs de l'époque pouvaient utiliser plusieurs processeurs identiques, mais il fallait que la carte mère le supporte. Et les cartes mères avec plusieurs sockets identiques étaient rares. Les cartes mères pour serveur en étaient capables, mais pas les cartes mères grand public. Les cartes mères plus grand public avaient cependant une option pour gérer plusieurs processeurs. Quelques ordinateurs assez anciens disposaient de '''coprocesseurs''', des processeurs secondaires qui complémentaient un processeur principal. Nous en avions déjà vu dans le chapitre sur l'architecture de base, et avions vu quelques exemples, provenant tous d'anciennes consoles de jeu. Mais il est temps de voir ces coprocesseurs en détail
==Les différents types de coprocesseurs : son, IO, FPU==
Le CPU et le coprocesseur sont foncièrement différents : ils n'ont pas le même jeu d'instruction, n'ont pas le même rôle, et j'en passe. Sauf exception, le CPU et le coprocesseur exécutent des programmes différents, ils travaillent en parallèle. Le coprocesseur a donc un rôle secondaire, reste à voir à quoi il sert.
===Les différents types de coprocesseurs===
Les coprocesseurs peuvent se classer en plusieurs catégories : les coprocesseurs sonores, arithmétiques, et d'entrées-sorties.
* Les '''coprocesseurs sonores''' sont une sorte d'ancêtre des cartes son, utilisés autrefois sur les consoles de jeu.
* Les '''coprocesseurs d'entrée-sortie''' ou ''Channel IO'', délèguent la gestion des entrée-sorties à un coprocesseur.
* Les '''coprocesseurs arithmétiques''' sont spécialisés dans les calculs, et notamment dans les calculs flottants.
Les '''coprocesseurs sonores''' ont eu leur heure de gloire sur les anciennes consoles de jeux vidéo, comme La Nintendo 64, la Playstation et autres consoles antérieures. Ils s'occupaient respectivement de calculer tout ce qui a trait au son. Pour donner un exemple, on peut citer la console Neo-géo, qui disposait de deux processeurs travaillant en parallèle : un processeur principal, et un coprocesseur sonore. Le processeur principal était un Motorola 68000, alors que le coprocesseur sonore était un processeur Z80.
Les '''coprocesseur d'IO''' sont dédiés à l'accès aux entrées-sorties. Pour simplifier, ce sont des contrôleurs DMA programmables, capables d'effectuer quelques opérations de branchements et de calcul. Nous en avions parlé dans le chapitre sur les entrée-sorties, je ne reviendrais pas dessus ici.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les '''coprocesseurs arithmétiques''' sont dédiés aux calculs en virgule flottante. Ils étaient utilisés à une époque où les CPU ne géraient que des calculs entiers (en binaire ou en BCD). Un exemple est le coprocesseur flottant x87, complémentaire des premiers processeurs Intel x86. Il y a eu la même chose sur les processeurs Motorola 68000, avec deux coprocesseurs flottants appelés les Motorola 68881 et les Motorola 68882. Ils sont aujourd'hui tombés en désuétude, depuis que les CPU sont devenus capables de faire des calculs sur des nombres flottants.
===Les coprocesseurs faiblement et fortement couplés===
Un coprocesseur peut être un processeur fait sur mesure, qui ne peut que servir de processeur secondaire, comme le sont tous les coprocesseurs arithmétiques. À l'inverse, de nombreuses consoles de jeu utilisaient un processeur des plus banals comme coprocesseur. Par exemple, de nombreuses consoles avaient un processeur principal, complété par un coprocesseur Z80 en guise de carte son. Et cette distinction est très liée à celle qui distingue les coprocesseurs faiblement couplés, et fortement couplés.
Le cas le plus fréquent est celui où le CPU et le coprocesseur travaillent en parallèle et exécutent des programmes différents. On parle alors de '''coprocesseurs faiblement couplés'''. Les coprocesseurs sonores et d'IO sont tous dans ce cas. Le coprocesseur sonore exécute un programme pour gérer le son, qui est séparé au programme principal. Le programme principal communique avec le coprocesseur, mais c'est assez rare. Dans un jeu vidéo, cela arrive seulement quand il faut changer de musique ou déclencher un effet sonore.
Les deux travaillent en parallèle, et il existe des mécanismes de synchronisation entre les deux processeurs. Typiquement, la synchronisation se fait en utilisant des ''interruptions inter-processeurs'', à savoir qu'un processeur exécute une interruption qui est exécutée par l'autre processeur. L'implémentation demande d'ajouter des circuits pour gérer ces interruptions entre processeurs.
Un exemple est celui des consoles néo-géo et Megadrive. Elles intègrent deux processeurs : un Motorola 68000 qui sert de processeur principal, un Z80 qui sert de processeur dédié à l'audio. Le MC68000 envoie des commandes au Z80, mais la communication ne va pas dans l'autre sens. Les deux processeurs communiquent via l'intermédiaire d'un ''IO arbiter chip'', qui gère les interruptions inter-processeur. Il contient un registre de 8 bits, dans lequel le MC68000 peut écrire un numéro d'interruption, qui indiquent quelle routine d'interruption exécuter. Lorsque le MC68000 écrit une valeur dedans, cela déclenche l’exécution automatique d'une interruption sur le Z80.
[[File:Architecture de la Megadrive et de la Néogeo.png|centre|vignette|upright=2.5|Architecture de la Megadrive et de la Néogeo]]
Les coprocesseurs arithmétiques se distinguent des autres, car ils fonctionnent en tandem avec le processeur principal, pas en parallèle. Les coprocesseurs précédents sont autonomes, à savoir qu'ils exécutent un programme différent de celui exécuté par le CPU. Mais les coprocesseurs arithmétiques ne sont pas dans ce cas. Il n'y a qu'un seul programme à exécuter, qui contient des instructions à destination du CPU, d'autres à destination du coprocesseur. Les instructions sont exécutées soit par le CPU, soit par le coprocesseur, une par une. En clair, le CPU et le coprocesseur se passent à la main à tour de rôle, ils ne travaillent pas en parallèle. On parle alors de '''coprocesseurs fortement couplés'''.
==Les coprocesseurs arithmétiques : généralités et exemples==
Dans le reste de ce chapitre, nous allons surtout voir les coprocesseurs arithmétiques, et ce pour plusieurs raisons. Premièrement, on a plus de documentation dessus, car c'était des processeurs commerciaux, vendus autrefois en magasin, avec de la documentation destinée aux utilisateurs. À l'opposé, les coprocesseurs sonores et d'IO étaient surtout utilisées sur des consoles de jeu, ou des gros ''mainframes''. Et autant les ''mainframes'' avaient de la documentation bien fournie, autant celle pour les consoles de jeu était très limitée. Une autre raison est que les autres coprocesseurs devraient être vus idéalement dans les chapitres sur les périphériques et entrées-sorties. C'est assez évident pour les coprocesseurs d'IO, mais c'est aussi l'idéal pour les coprocesseurs sonores. En effet, ceux-ci sont en quelque sorte des cartes sons partielles.
===Les anciens co-processeurs flottants des PCs===
Les coprocesseurs arithmétiques étaient spécialisés dans les calculs flottants et étaient généralement fortement couplés. Les coprocesseurs étaient optionnels et il était parfaitement possible de monter un PC qui n'en avait pas. En conséquence, les programmeurs devaient coder des programmes qui peuvent fonctionner avec et sans coprocesseur. La solution la plus simple était de fournir deux versions du logiciel : une sans usage du coprocesseur, et une autre qui en fait usage, plus rapide. Une autre solution était d'émuler les calculs flottants en logiciel.
Un bon exemple de la seconde solution est le 68881 de Motorola, conçu pour fonctionner avec les CPU 6802 et 68030. Les programmes mixaient instructions entières et flottantes, le 68000 exécutant les instructions entières, le 68881 exécutant les instructions flottantes. Les instructions flottantes avaient un opcode qui commençait par F (en hexadécimal), ce qui permettait de les distinguer rapidement du reste. Le 68000 chargeait les instructions, et regardait si l'instruction était destinée soit au coprocesseur, soit pour lui. Si elle était pour le coprocesseur, le 68000 lisait les opérandes depuis la mémoire et envoyait tout au coprocesseur 68881, qui exécutait l'instruction, puis rendait la main au 68000.
Les coprocesseurs Motorola utilisaient des flottants codés sur 80 bits : la mantisse était codée sur 64 bits, l'exposant sur 15 bits. Le 68881 incorporait 8 registres flottants, nommés, de 80 bits chacun. Il avait aussi un registre de statut et un registre de contrôle, guère plus. Fait étonnant, les coprocesseurs pour les PC faisaient pareil : flottants codés sur 80 bits, 8 registres flottants. Mais les détails sont différents, comme on le verra plus tard.
===Les coprocesseurs arithmétiques des consoles de jeu===
Les consoles de jeu incorporent souvent des coprocesseurs. Mais il est assez rare qu'elles incorporent des coprocesseurs arithmétiques et c'est surtout quelque chose qu'on observe sur les consoles des années 2000. La raison est que les consoles utilisent des processeurs commerciaux, utilisés dans les smartphones ou les PC. Elles n'utilisent pas de processeur spécifiquement conçu pour elles, sauf exceptions. Et de tels processeurs disposent de capacités de calcul flottant ou entières suffisantes, ils sont choisis pour. Mais il existe quelques cas où ce n'est pas le cas.
Un exemple récent est de la console de jeu Nintendo DS. La console utilisait deux processeurs, un ARM9 et un ARM7, deux processeurs RISC qui ne pouvaient pas faire de division entière. Il s'agit pourtant d'opérations importantes dans le cas du rendu 3D, ce qui fait que les concepteurs de la console ont rajouté un coprocesseur spécialisé dans les divisions entières et les racines carrées. Le coprocesseur était adressable directement par le processeur, comme peuvent l'être la RAM ou les périphériques, et était traité comme une entrée-sortie comme une autre.
Un autre exemple est celui du processeur CELL de la console de jeu PS3. Il était conçu spécifiquement pour cette console. Il intègre un cœur principal POWER PC v5 et 8 cœurs qui servent de processeurs auxiliaires. Le processeur principal est appelé le PPE et les processeurs auxiliaires sont les SPE. Les SPE sont reliés à une mémoire locale (''local store'') de 256 kibioctets qui communique avec le processeur principal via un bus spécial. Cette fois-ci, les coprocesseurs sont intégrés dans le même processeur.
Les SPE communiquent avec la RAM principale via des contrôleurs DMA. Les SPE possèdent des instructions permettant de commander leur contrôleur DMA et c'est le seul moyen qu'ils ont pour récupérer des informations depuis la mémoire. Et c'est au programmeur de gérer tout ça ! C'est le processeur principal qui va envoyer aux SPE les programmes qu'ils doivent exécuter. Il délègue des calculs aux SPE en écrivant dans le local store du SPE et en lui ordonnant l’exécution du programme qu'il vient d'écrire.
[[File:Schema Cell.png|centre|vignette|upright=2|Architecture du processeur CELL de la PS3. Le PPE est le processeur principal, les SPE sont des processeurs auxiliaires qui comprennent : un ''local store'' noté LS, un processeur noté SXU, et un contrôleur DMA pour échanger des informations avec la mémoire principale.]]
==Le jeu d'instruction x87 d'Intel==
Un exemple de coprocesseur arithmétique est celui de l'extension x87 qui a ajouté les nombres flottants aux processeurs x86. Pour rappel, les processeurs x86 sont ceux qui sont présents à l'intérieur des PC, à savoir tous les processeurs Intel et AMD actuels. Il s'agit d'un jeu d'instruction qui a reçu de nombreux ajouts au cours du temps. Les ajouts en question sont appelés des '''extensions x86'''. Nous allons maintenant voir celle qui a ajouté la gestion des flottants au x86. L'extension x87 n'est plus utilisée depuis l'arrivée des CPU 64 bits, car elle a été remplacée par l'extension SSE.
Sur les tout premiers processeurs x86, le support des nombres flottants n'était pas implémenté. A la place, ils utilisaient des coprocesseurs arithmétiques, appelés des '''coprocesseurs x87'''. Ils travaillaient en tandem avec un processeur x86 normal, et ne géraient que des instructions arithmétiques sur des flottants. Ils étaient capables d'exécuter les 4 opérations de base (add, sub, mul, div), la racine carrée, les opérations trigonométriques sinus, cosinus et tangente, l'arc tangente, et des instructions de calcul de logarithmes ou d'exponentielles.
===La pseudo-pile de registres===
Les coprocesseurs arithmétiques x87 avaient 8 registres flottants, qui étaient gérés avec une pseudo-pile, ainsi que 3 registres de contrôle. Les 8 registres x87 sont ordonnés et numérotés de 0 à 7. Les registres x87 sont organisés sous la forme d'une pile de registres, similaire à une pile d'assiette, que l'on remplit dans un ordre bien précis. Lors du chargement d'un opérande de la RAM vers les registres, il n'est pas possible de décider du registre de destination. Si on veut ajouter des flottants dans nos registres, on doit les « remplir » dans un ordre de remplissage imposé : on remplit d'abord le registre 7, puis le 6, puis le 5, et ainsi de suite jusqu'au registre 0. Si on veut ajouter un flottant dans cette pile de registres, celui-ci sera stocké dans le premier registre vide dans l'ordre de remplissage indiqué au-dessus.
Prenons un exemple, les 3 premiers registres sont occupés par un flottant et on veut charger un flottant supplémentaire : le 4e registre sera utilisé pour stocker ce flottant. La même chose existe pour le « déremplissage » des registres. Imaginez que vous souhaitez déplacer le contenu d'un registre dans la mémoire RAM et effacer complètement son contenu. On ne peut pas choisir n'importe quel registre pour faire cela : on est obligé de prendre le registre non vide ayant le numéro le plus grand.
{|
|[[File:Pseudo-pile x87. - PUSH.png|vignette|upright=2|Pseudo-pile x87 - chargement d'une opérande.]]
|[[File:Pseudo-pile x87 - POP.png|vignette|upright=2|Pseudo-pile x87 - retrait d'une opérande.]]
|}
Les instructions à un opérande dépilent le flottant au sommet de la pile. Les instructions dyadiques peuvent dépiler les deux opérandes au sommet de la pile, mais elles peuvent aussi utiliser d'autres modes d'adressage. Elles peuvent aller chercher la seconde opérande en RAM, en fournissant une adresse. Mais elles peuvent aussi adresser n'importe quel autre registre de la pile en fournissant son numéro de registre. Avec ce dernier mode d'adressage, le processeur agit comme une sorte de processeur à accumulateur, avec le sommet de la pile servant d'accumulateur.
En somme, les registres de la pile sont adressables, du moins pour ce qui est de gérer la seconde opérande. C'est cette particularité qui vaut le nom de pseudo-pile à cette organisation à mi-chemin entre une pile et une architecture à registres.
===Les instructions x87===
Maintenant, voyons quelles instructions les FPU x87 devaient gérer. L'extension x87 est en effet un jeu d'instruction, qui décrit quelles instructions doivent être gérées. Les instructions utilisent des opcodes inutilisés dans le jeu d'instruction x86, qui sont détournés pour fonctionner sur le x87.
On trouve évidemment des instructions de calculs, bien évidemment compatibles avec la norme IEE754 : l'addition FADD, la soustraction FSUB, la multiplication FMUL et la division FDIV. Mais il y a aussi la racine carrée FSQRT. La FPU x87 implémente aussi des instructions trigonométriques, qui ne sont pas supportées par la norme IEEE 754. Elle gérait l'instruction FCOS pour le cosinus, l'instruction FSIN pour le sinus, l'instruction FPTAN pour la tangente, l'instruction FPATAN pour l'arc tangente, ainsi que des instructions de calcul de logarithmes ou d'exponentielles.
La FPU x87 dispose aussi d'instructions de comparaisons compatibles avec la norme IEEE 754, capables de comparer le flottant au sommet de la pile avec un autre nombre qui peut être flottant ou entier ! Voici une liste de quelques instructions de comparaisons supportées par les FPU 87 :
* FCOM : compare le contenu du registre 0 avec une constante flottante ;
* FCOMI : compare le contenu des registres 0 et 1 ;
* FICOM : compare le contenu du registre 0 avec une constante entière ;
* FTST : compare le registre numéroté 0 avec la valeur 0.
Le x87 avait aussi des instructions de calcul de la valeur absolue (FABS) ou encore de changement de signe (FCHS).
Les instructions de calcul n'ayant besoin que d'un seul flottant pour s'exécuter, comme les opérations trigonométriques ou la valeur absolue, utilisent le flottant situé au sommet de la pile. Les instructions dyadiques (multiplication, addition, soustraction et autres) vont agir différemment suivant la situation. Elles peuvent prendre les deux flottants les plus haut placés dans cette pile, prendre le flottant au sommet de la pile, utiliser une donnée en provenance de la mémoire, ou encore utiliser le flottant le plus haut placé et un flottant stocké dans l'importe quel registre de cette pile de registres. La pile de registre était donc une sorte de mélange entre un accumulateur et 7 registres adressables.
Pour charger des opérandes dans la pile d'opérande, l'extension x87 fournit trois instructions d'accès mémoire.
{|class="wikitable"
|-
! Instruction
! Description
|-
! FLD
| Charge un nombre flottant depuis la mémoire vers notre pile de registres vue au-dessus. Cette instruction peut charger un flottant codé sur 32 bits, 64 bits ou 80 bits
|-
! FSTP
| Déplace le contenu d'un registre vers la mémoire. Une autre instruction existe qui est capable de copier le contenu d'un registre vers la mémoire sans effacer le contenu du registre : c'est l'instruction FST
|-
! FXCH
| Échange le contenu du dernier registre non vide dans l'ordre de remplissage (celui situé au sommet de la pile) avec un autre registre
|}
D'autres instructions existent qui chargent certaines constantes (PI, 1, 0, certains logarithmes en base 2) dans le registre au sommet de la pile de registres.
===Les registres de contrôle et d'état===
Pour gérer la pseudo-pile, les registres pour les flottants sont associés à un registre d'état nommé '''''Tag Word'''''. Le registre ''Tag Word'' indique, pour chaque registre flottant, s'il est vide ou non. Avouez que c'est pratique pour gérer la pile de registres vue au-dessus ! Le registre ''tag word'' fait 16 bits, ce qui fait 2 bits pour chacun des 8 registres. Ces deux bits contiennent des informations sur le contenu du registre de données réservé.
* Si ces deux bits valent 00, le registre contient un flottant « normal » différent de zéro ;
* Si ces deux bits valent 01, le registre contient une valeur nulle : 0 ;
* Si ces deux bits valent 10, le registre contient un NAN, un infini, ou un dénormal ;
* Si ces deux bits valent 11, le registre est vide et ne contient pas de nombre flottant.
Le processeur x87 contient aussi deux registres d'état, nommés ''Control Word'' et ''Status Word''. Le registre ''Status Word'' contient quelques bits, certains utilisés pour gérer la pseudo-pile, d'autres non. Il fait lui aussi 16 bits et c'est un registre d'état qui est utilisé pour qu'un programme puisse comprendre la cause d'une exception. Il contient le numéro du registre juste au-dessus du sommet de la pile, le numéro du premier registre vide dans l'ordre de remplissage. Mais il contient surtout des bits mis à 1 en cas de débordement de flottant, de division par zéro, lorsqu'un calcul a pour résultat un dénormal, etc.
{|class="wikitable"
|-
! Bit !! Utilité
|-
! TOP
| Trois bits, qui codent le numéro du premier registre vide dans la pile de registre
|-
! U
| Détecte les ''underflows'' : est mis à 1 en cas d'''underflows''.
|-
! O
| | Détecte les ''overflows'' : est mis à 1 en cas d'''overflows''.
|-
! Z
| Prévient qu'une division par zéro a eu lieu. Est mis à 1 si c'est le cas.
|-
! D
| Bit est mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal
|-
! I
| Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0
|}
Le registre ''Control Word'' fait 16 bits et configure la gestion des arrondis.
{|class="wikitable"
|-
! Bit !! Utilité
|-
! Infinity Control
| S'il vaut zéro, les infinis sont tous traités comme s'ils valaient +∞. S'il vaut un, les infinis sont traités normalement
|-
! Rouding Control
| C'est un ensemble de deux bits qui détermine le mode d'arrondi utilisé
* 00 : vers le nombre flottant le plus proche : c'est la valeur par défaut ;
* 01 : vers - l'infini ;
* 10 : vers + l'infini ;
* 11 : vers zéro
|-
! Precision Control
|`Ensemble de deux bits qui détermine la taille de la mantisse de l'arrondi du résultat d'un calcul. En effet, on peut demander à notre FPU d'arrondir le résultat de chaque calcul qu'elle effectue. Cette instruction ne touche pas à l'exposant, mais seulement à la mantisse. La valeur par défaut de ces deux bits est 11 : notre FPU utilise donc des flottants double précision étendue. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754.
* 00 : mantisse codée sur 24 bits ;
* 01 : valeur inutilisée ;
* 10 : mantisse codée sur 53 bits ;
* 11 : mantisse codée sur 64 bits
|}
Les instructions x87 sont codées sur au minimum deux octets. Le premier octet commence toujours la suite de bit 11011, qui indique que c'est une instruction destinée au coprocesseur. Le 11011 était appelé le code d'échappement, sa mnémonique en assembleur était ESC. Le tout est suivi par 6 bits d'opcode, et 5 bits pour le mode d'adressage. Le tout était regroupé comme suit : 1101 1xxx aaxx xaaa, avec x les bits de l'opcode et a pour le mode d'adressage. Le tout était suivi par des opérandes, selon le mode d'adressage.
==Les coprocesseurs x87 et leurs précurseurs==
Les premiers coprocesseurs de ce type étaient l'Intel 8231/8232, destinés à être utilisés avec le 8088, un processeur 8 bits. Par la suite, Intel a récidivé avec le 8087, qui était destiné pour servir en tandem avec le 8086. Il a été suivi par les Intel 187, le 287, le 387, le 487 et le 587, qui étaient censés servir avec les CPU 186, 286, 386, 486, etc. Mais d'autres compagnies ont crée des coprocesseurs x87, comme Weitek, Cyrix, AMD, Texas Instrument, et bien d'autres.
===Les précurseurs : l'Intel 8231 et le 8232===
L'Am9511 et Am9512 sont une des toutes premières FPU pour PC, si ce n'est les premières si on en croit AMD. Ils ont été licenciés par Intel sous le nom d'Intel 8231 et le 8232. Ils étaient conçus pour complémenter le CPU Intel 8080, mais on pouvait parfaitement les utiliser avec d'autres CPU, comme le Z80. La [http://ep.homeserver.hu/PDF/AM9511A-9512.pdf documentation AMD donnait même des exemples assez variés]. La raison est qu'on y accédait comme n'importe quelle entrée-sortie connectée au bus système. Ils étaient accessibles via ''pooling'', interruptions ou même via DMA. Nous expliquerons comment c'est possible plus bas.
L'Intel 8231 ne supportait pas le jeu d'extension x87, qui est apparu après. Il gérait des nombres flottants de 32 bits, mais aussi des nombres en virgule fixe de 16 et 32 bits. Les flottants 32 bits suivaient globalement la norme IEEE 754, mais les nombres en virgule fixe utilisaient un format propriétaire. Il gérait les quatre opérations de base, mais aussi des calculs trigonométriques. Il utilisait pour cela du microcode, avec une approximation basée sur des polynômes de Tchebychev. L'intel 8232 supportait lui des flottants 32 et 64 bits, mais ne supportait que les quatre opérations de base (addition, soustraction, multiplication et division).
: La documentation décrit ces flottants 32 bits comme étant de la double précision, mais c'est parce que la terminologie de l'époque n'était pas encore bien stabilisée.
[[File:C8231A FPU PIN CONFIGURATION.png|thumb|Intel 8231 FPU PIN CONFIGURATION.]]
Les broches de l'intel 8231 sont illustrées ci-contre. La plupart des broches nous sont familières : 8 broches pour le bus de données (qui fait 8 bits), une entrée d'horloge, une entrée de RESET, une entrée ''chip select'' pour le décodage d'adresse. Les broches restantes sont très intéressantes, mais on les verra dans ce qui suit.
Toujours est-il que le coprocesseur est relié à un bus de 8 bits, alors que ses registres font 32 à 64 bits. Pour cela, le 8231/8232 lisait les opérandes octet par octet depuis le bus de données. Idem mais pour les écritures. Par contre, les instructions sont prévues pour faire 8 bits, pas plus. Pour avoir des instructions aussi courtes, la seule solution est d'utiliser une machine à pile et c'est ce que le 8231/8232 a fait. Précisons cependant que ce n'est pas la même pile de registre que la pile x87, mais c'était une sorte de pile similaire, qui a évolué pour donner la pile x87. Il s'agissait pour le coup d'une vraie pile, les opérations utilisaient systématiquement le sommet de la pile et l'opérande en dessous. Il n'y avait pas de possibilité d'adresser un opérande dans la pile.
Le processeur intégrait 8 registres de 16 bits, organisés comme une pile. Les registres pouvaient être utilisés : soit comme une pile de 8 opérandes 16 bits, soit une pile de 4 opérandes 32 bits, soit une pile de 2 opérandes 64 bits. Les opérandes étaient empilées octet par octet dans le processeur. Ils étaient dépilés là aussi octet par octet. Pour cela, le 8231/8232 dispose de trois entrées nommées A0, RD et WR. Les trois bits décident s'il faut faire une lecture, une écriture, exécuter une instruction, ou lire le registre d'état. Les quatre opérations sont appelées des commandes dans la documentation Intel et AMD. Les trois entrées font donc office de bus de commande simplifié.
{|class="wikitable"
|-
! A0, RD, WR !! Action
|-
| 000 || Lecture de l'octet depuis les registres de données.
|-
| 010 || Écriture de l'octet dans les registres de données.
|-
| 111 || L'octet est l'opcode d'une instruction, qui est exécutée immédiatement.
|-
| 101 || Lecture du registre d'état.
|}
Le processeur principal envoyait des commandes à l'Intel 8231/8232, qui les exécutait dans son coin. Le 8231/8232 envoyait un signal END OF EXECUTION pour prévenir qu'il avait fini son travail, que la commande précédente était terminée. Il avait une broche dédiée, appelée END, dédiée à ça. Le coprocesseur avait donc une interface de communication asynchrone, qui se voit quand on étudie ses broches. Les broches suivantes servent à la communication asynchrone avec le 8231/8232.
* READY est à 1 quand le 8231/8232 est libre, capable d'accepter une nouvelle instruction/commande. Il passe à 0 quand une instruction démarre, avec la commande 111 vue plus haut.
* END indique que la commande précédente a terminé son exécution. Lorsque END passe à 1, BUSY passe automatiquement à 0.
* EACK est une entrée sur laquelle le processeur dit qu'il a bien reçu le signal END, et que ce dernier peut être remis à 0.
Ce système pouvait être utilisé avec du ''pooling'', avec des interruptions, voire du DMA. Avec des interruptions, la sortie END était utilisée comme sortie d'interruption, reliée au CPU ou au contrôleur d'interruption. Pour le ''pooling'', le registre d'état du 8231/8232 contenait un bit BUSY, qui indiquait si le coprocesseur était utilisé ou non.
Un tel fonctionnement peut sembler étrange, et vous aurez l'impression que communiquer avec le coprocesseur est très lent. Mais cela prend tout son sens quand on connait le temps mis pour exécuter une instruction sur le coprocesseur. Une opération simple sur des flottants 32 bits prenait facilement une cinquantaine de cycles d'horloge, et c'était parmi les meilleurs temps de calcul. Il n'était pas rare d'avoir des opérations prenant plusieurs centaines, voire milliers de cycles d'horloge. Pas loin de 5000 cycles d'horloge pour une division de deux flottants 64 bits sur le 8232, plusieurs dizaines de milliers de cycles pour certaines opérations trigonométriques. Et le pire, c'était que c'était plus rapide que l'émulation logicielle !
Pas étonnant donc que le 8231/8232 aient été traités comme des entrées-sorties, à une époque ou tout était connecté sur un bus système assez rapide. Un autre avantage est que le 8231/8232 pouvaient fonctionner à une fréquence sans rapport avec celle du processeur. Par exemple, on pouvait utiliser un processeur à 1 MHz alors que le 8231/8232 allait à 4 MHz. Le coprocesseur faisait juste des calculs rapidement, comparé au CPU. Et ça a été utilisé sur certains systèmes Apple II. Ou encore, on pouvait utiliser un processeur légèrement plus rapide que le coprocesseur, avec quelques MHz de différence, comme un CPU à 5 MHz avec un coprocesseur de 2 MHz.
===L'intel 8087 et ses successeurs===
[[File:Intel 8087.svg|vignette|Intel 8087]]
Le 8087 été fabriqué avec 65 000 transistors. Le 8087 avait pour particularité qu'il était connecté directement sur le bus mémoire, au même titre que le 8086. Mais le 8087 n'avait pas de bus d'adresse et de données séparé. Le processeur utilisait un bus multiplexé. Il avait 20 broches pour se connecter au bus : 16 d'entre elles servaient alternativement de bus d'adresse et de données. L'interface avec le bus était donc un peu compliquée.
L'intel 387 était le coprocesseur associé au 386 d'Intel. Il était le premier coprocesseur à s'intégrer sur un bus de 32 bits. Il a été décliné en plusieurs versions, dont certaines sont spécifiques à un modèle de 386. Par exemple, le i386SX était une version simplifiée du 386 initial, qui avait notamment un bus de seulement 16 bits. Et de ce fait, il avait son propre coprocesseur i387SX, qui était adapté à un bus de 16 bits. De même, le i386SL était adapté aux ordinateurs portables et avait son propre coprocesseur i387SL.
Tout ce qui va suivre est valide pour tous les coprocesseurs x87 de marque Intel.
Le processeur central lisait des instructions, en envoyant le ''program counter'' sur le bus d'adresse, les instructions étaient récupérées sur le bus de données. Là, les deux processeurs déterminaient si l'instruction chargée était destinée au coprocesseur ou au CPU. Pour cela, les instructions x87 commencent toutes par la suite de bit 11011, qui permet de savoir facilement si une instruction est destinée au coprocesseur. Le 11011 était suivi par un opcode et un mode d'adressage.
Si le mode d'adressage demandait de lire un opérande mémoire, le 8086 envoyait l'adresse de l'opérande sur le bus, et le coprocesseur récupérait celle-ci sur le bus de données. Si l'opérande devait être lu en plusieurs fois, le coprocesseur lisait le reste de lui-même, en prenant le contrôle du bus d'adresse. Il récupérait l'adresse envoyée initialement par le CPU, puis l'incrémentait et relançait un nouvel accès mémoire. Il l'incrémentait autant de fois que nécessaire pour charger l'opérande.
Un problème est que le CPU ne sait pas combien de temps dure une instruction x87. Et cette durée dépendait de l'implémentation du processeur, elle n'était pas la même selon la marque du coprocesseur. Un 186 n'avait pas les mêmes timings que le 286, par exemple. Pour le CPU, une instruction x87 met juste deux cycles pour s'exécuter (plus si des opérandes doivent être lus en mémoire). Pour cela, le CPU disposait d'un mécanisme de synchronisation.
Le mécanisme de synchronisation était une instruction WAIT, qui forçait le CPU à attendre que le coprocesseur ait terminé l'instruction précédente. L'implémentation matérielle était assez simple. Le coprocesseur disposait d'une sortie BUSY, qui indiquait qu'il était en train d'exécuter une instruction et ne pouvait pas en accepter une nouvelle. Le CPU, quant à lui, avait une entrée TEST qui vérifiait si le, coprocesseur était occupé ou non. La sortie BUSY était reliée à l'entrée TEST. L'instruction test vérifiait juste ce qu'il y avait sur l'entrée TEST. Tant qu'elle était à 1, le processeur attendait et ne chargeait pas de nouvelle instruction. Dès qu'elle passe à 0, l'exécution reprend.
Il faut noter que l'instruction WAIT n'est nécessaire qu'entre deux instructions flottantes assez proches. Mais il est possible d'intercaler des instructions entières entre deux instructions flottantes. Le programme pouvait ainsi mixer instructions entières et flottantes, les instructions entières étant exécutées sur le 8086, les instructions flottantes sur le coprocesseur. Il y avait donc une possibilité de parallélisme, à savoir que les deux processeurs pouvaient exécuter des instructions différentes en même temps. Mais cela demandait que les calculs soient coopératifs et mélangent bien entiers et flottants.
Le 8087 et ses successeurs avaient une microarchitecture assez simple. L'unité de contrôle contenait un décodeur d'instruction microcodé, le registre de contrôle, le registre d'état. La plupart des instructions sont microcodées, l'unité de calcul est assez limitée. Elle permet d'additionner deux mantisses flottantes, de faire des décalages, d'additionner deux exposants, mais pas plus. Les multiplications et divisions sont donc microcodées et émulées en enchainant des additions flottantes. Les instructions trigonométriques sont implémentées en utilisant l'algorithme CORDIC, qu'on a vu dans le chapitre sur les circuits de calcul flottant.
[[File:Intel 8087 arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel 8087.]]
Le chemin de données est composé d'un banc de registre flottant pour la pseudo-pile, et de plusieurs circuits de calcul. Le banc de registre était mono-port, ce qui fait que les ALUs étaient précédés par deux registres temporaires pour les opérandes. Les circuits pour l'exposant et la mantisse sont séparés, et sont même reliés au banc de registre par deux bus séparés. Il y a un additionneur pour les exposants, un additionneur pour les mantisses et un décaleur pour les mantisses (pour les normaliser).
De plus, on trouve une mémoire ROM dédiée aux constantes les plus utilisées. Elle sert pour les constantes de base, gérées par le jeu d'extension x87. Mais elle contient aussi des constantes utilisées pour l'algorithme CORDIC. Elle n'est pas illustrée sur le schéma ci-dessous, mais elle existe.
L'interface avec le bus est un simple registre d’interfaçage avec le bus. Pour rappel, le bus de données fait 16 bits sur le 8087, 32 bits sur le 387. Entre le bus et le chemin de données, on trouve une file servant à simplifier la gestion des lectures. L'idée est que les opérandes lus/écrits font 32, 64 ou 80 bits, alors que le bus de données n'en fait que 16/32. Les opérandes sont donc lus/écrits en plusieurs passes. Sur le 8087, il doit réaliser deux passes pour des opérandes de 32 bits, quatre passes pour celles de 64 bits, 5 pour des opérandes de 80 bits (80 = 5 × 16). Le 387 doit faire deux fois moins.
[[File:Intel 387 arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel 387. Les circuits pour les exposants sont à gauche dans le chemin de données, les circuits pour les mantisses sont à droite.]]
L'implémentation du banc de registre est assez simple : une RAM avec un registre qui indique la position du sommet de la pile dedans. Le registre fait 3 bits, pour 8 registres. En plus de cela, il y a un petit soustracteur et un multiplexeur, pour adresser les opérandes dans la pile. Pour rappel, il est possible d'adresser la seconde opérande dans la pile. Mais on précise pas le numéro du registre dans la pile pour cela, on précise sa position sous le sommet de la pile, à savoir si elle est deux, trois, quatre opérandes sous le sommet de la pile. Pour déterminer quel registre lire, il faut soustraire ce "décalage" au numéro de registre du sommet de la pile. Pour cela, il y a un petit soustracteur pour faire le calcul.
Le circuit décaleur est composé de deux sous-décaleurs. Le premier fait des décalages au niveau des octets, le second décale l'opérande de 0 à 7 rangs.
Pour finir, voici quelques liens sur la microarchitecture du 8087 :
* [https://www.righto.com/2026/02/8087-instruction-decoding.html Instruction decoding in the Intel 8087 floating-point chip].
* [https://www.righto.com/2020/05/die-analysis-of-8087-math-coprocessors.html Die analysis of the 8087 math coprocessor's fast bit shifter].
* [https://www.righto.com/2025/12/8087-stack-circuitry.html The stack circuitry of the Intel 8087 floating point chip, reverse-engineered].
* [https://www.righto.com/2025/12/8087-microcode-conditions.html Conditions in the Intel 8087 floating-point chip's microcode].
* [https://www.righto.com/2020/05/extracting-rom-constants-from-8087-math.html Extracting ROM constants from the 8087 math coprocessor's die].
* [https://www.righto.com/2018/09/two-bits-per-transistor-high-density.html Two bits per transistor: high-density ROM in Intel's 8087 floating point chip].
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les sections critiques et le modèle mémoire
| prevText=Les sections critiques et le modèle mémoire
| next=L'accélération matérielle de la virtualisation
| nextText=L'accélération matérielle de la virtualisation
}}
</noinclude>
gtsgbvlwbk1g454uyhay3t96ktosksw
763007
763006
2026-04-05T18:34:05Z
Mewtow
31375
/* Les différents types de coprocesseurs */
763007
wikitext
text/x-wiki
Les processeurs actuels sont des processeurs multicœurs, à savoir qu'ils intègrent plusieurs processeurs dans un seul circuit intégré. Du moins, c'est une explication simplifiée, des circuits sont mutualisés entre les processeurs. Les cœurs sont tous identiques, à savoir qu'ils ont le même jeu d'instruction, ont une microarchitecture similaire, etc. Mais ce n'est possible que parce que les processeurs actuels ont un grand nombre de transistors, grâce à la loi de Moore. Pour intégrer plusieurs cœurs sur un processeur, il faut le budget en transistor pour. Mais au siècle dernier, le budget en transistors n'était pas là. Aussi, utiliser plusieurs processeurs était plus compliqué.
Les ordinateurs de l'époque pouvaient utiliser plusieurs processeurs identiques, mais il fallait que la carte mère le supporte. Et les cartes mères avec plusieurs sockets identiques étaient rares. Les cartes mères pour serveur en étaient capables, mais pas les cartes mères grand public. Les cartes mères plus grand public avaient cependant une option pour gérer plusieurs processeurs. Quelques ordinateurs assez anciens disposaient de '''coprocesseurs''', des processeurs secondaires qui complémentaient un processeur principal. Nous en avions déjà vu dans le chapitre sur l'architecture de base, et avions vu quelques exemples, provenant tous d'anciennes consoles de jeu. Mais il est temps de voir ces coprocesseurs en détail
==Les différents types de coprocesseurs : son, IO, FPU==
Le CPU et le coprocesseur sont foncièrement différents : ils n'ont pas le même jeu d'instruction, n'ont pas le même rôle, et j'en passe. Sauf exception, le CPU et le coprocesseur exécutent des programmes différents, ils travaillent en parallèle. Le coprocesseur a donc un rôle secondaire, reste à voir à quoi il sert.
===Les différents types de coprocesseurs===
Les coprocesseurs peuvent se classer en plusieurs catégories : les coprocesseurs sonores, arithmétiques, et d'entrées-sorties.
Les '''coprocesseurs sonores''' sont une sorte d'ancêtre des cartes son, utilisés sur les anciennes consoles de jeux vidéo, comme La Nintendo 64, la Playstation et autres consoles antérieures. Ils s'occupaient respectivement de calculer tout ce qui a trait au son. Pour donner un exemple, on peut citer la console Neo-géo, qui disposait de deux processeurs travaillant en parallèle : un processeur principal, et un coprocesseur sonore. Le processeur principal était un Motorola 68000, alors que le coprocesseur sonore était un processeur Z80.
Les '''coprocesseur d'IO''' sont dédiés à l'accès aux entrées-sorties. Pour simplifier, ce sont des contrôleurs DMA programmables, capables d'effectuer quelques opérations de branchements et de calcul. Nous en avions parlé dans le chapitre sur les entrée-sorties, je ne reviendrais pas dessus ici.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les '''coprocesseurs arithmétiques''' sont dédiés aux calculs en virgule flottante. Ils étaient utilisés à une époque où les CPU ne géraient que des calculs entiers (en binaire ou en BCD). Un exemple est le coprocesseur flottant x87, complémentaire des premiers processeurs Intel x86. Il y a eu la même chose sur les processeurs Motorola 68000, avec deux coprocesseurs flottants appelés les Motorola 68881 et les Motorola 68882. Ils sont aujourd'hui tombés en désuétude, depuis que les CPU sont devenus capables de faire des calculs sur des nombres flottants.
===Les coprocesseurs faiblement et fortement couplés===
Un coprocesseur peut être un processeur fait sur mesure, qui ne peut que servir de processeur secondaire, comme le sont tous les coprocesseurs arithmétiques. À l'inverse, de nombreuses consoles de jeu utilisaient un processeur des plus banals comme coprocesseur. Par exemple, de nombreuses consoles avaient un processeur principal, complété par un coprocesseur Z80 en guise de carte son. Et cette distinction est très liée à celle qui distingue les coprocesseurs faiblement couplés, et fortement couplés.
Le cas le plus fréquent est celui où le CPU et le coprocesseur travaillent en parallèle et exécutent des programmes différents. On parle alors de '''coprocesseurs faiblement couplés'''. Les coprocesseurs sonores et d'IO sont tous dans ce cas. Le coprocesseur sonore exécute un programme pour gérer le son, qui est séparé au programme principal. Le programme principal communique avec le coprocesseur, mais c'est assez rare. Dans un jeu vidéo, cela arrive seulement quand il faut changer de musique ou déclencher un effet sonore.
Les deux travaillent en parallèle, et il existe des mécanismes de synchronisation entre les deux processeurs. Typiquement, la synchronisation se fait en utilisant des ''interruptions inter-processeurs'', à savoir qu'un processeur exécute une interruption qui est exécutée par l'autre processeur. L'implémentation demande d'ajouter des circuits pour gérer ces interruptions entre processeurs.
Un exemple est celui des consoles néo-géo et Megadrive. Elles intègrent deux processeurs : un Motorola 68000 qui sert de processeur principal, un Z80 qui sert de processeur dédié à l'audio. Le MC68000 envoie des commandes au Z80, mais la communication ne va pas dans l'autre sens. Les deux processeurs communiquent via l'intermédiaire d'un ''IO arbiter chip'', qui gère les interruptions inter-processeur. Il contient un registre de 8 bits, dans lequel le MC68000 peut écrire un numéro d'interruption, qui indiquent quelle routine d'interruption exécuter. Lorsque le MC68000 écrit une valeur dedans, cela déclenche l’exécution automatique d'une interruption sur le Z80.
[[File:Architecture de la Megadrive et de la Néogeo.png|centre|vignette|upright=2.5|Architecture de la Megadrive et de la Néogeo]]
Les coprocesseurs arithmétiques se distinguent des autres, car ils fonctionnent en tandem avec le processeur principal, pas en parallèle. Les coprocesseurs précédents sont autonomes, à savoir qu'ils exécutent un programme différent de celui exécuté par le CPU. Mais les coprocesseurs arithmétiques ne sont pas dans ce cas. Il n'y a qu'un seul programme à exécuter, qui contient des instructions à destination du CPU, d'autres à destination du coprocesseur. Les instructions sont exécutées soit par le CPU, soit par le coprocesseur, une par une. En clair, le CPU et le coprocesseur se passent à la main à tour de rôle, ils ne travaillent pas en parallèle. On parle alors de '''coprocesseurs fortement couplés'''.
==Les coprocesseurs arithmétiques : généralités et exemples==
Dans le reste de ce chapitre, nous allons surtout voir les coprocesseurs arithmétiques, et ce pour plusieurs raisons. Premièrement, on a plus de documentation dessus, car c'était des processeurs commerciaux, vendus autrefois en magasin, avec de la documentation destinée aux utilisateurs. À l'opposé, les coprocesseurs sonores et d'IO étaient surtout utilisées sur des consoles de jeu, ou des gros ''mainframes''. Et autant les ''mainframes'' avaient de la documentation bien fournie, autant celle pour les consoles de jeu était très limitée. Une autre raison est que les autres coprocesseurs devraient être vus idéalement dans les chapitres sur les périphériques et entrées-sorties. C'est assez évident pour les coprocesseurs d'IO, mais c'est aussi l'idéal pour les coprocesseurs sonores. En effet, ceux-ci sont en quelque sorte des cartes sons partielles.
===Les anciens co-processeurs flottants des PCs===
Les coprocesseurs arithmétiques étaient spécialisés dans les calculs flottants et étaient généralement fortement couplés. Les coprocesseurs étaient optionnels et il était parfaitement possible de monter un PC qui n'en avait pas. En conséquence, les programmeurs devaient coder des programmes qui peuvent fonctionner avec et sans coprocesseur. La solution la plus simple était de fournir deux versions du logiciel : une sans usage du coprocesseur, et une autre qui en fait usage, plus rapide. Une autre solution était d'émuler les calculs flottants en logiciel.
Un bon exemple de la seconde solution est le 68881 de Motorola, conçu pour fonctionner avec les CPU 6802 et 68030. Les programmes mixaient instructions entières et flottantes, le 68000 exécutant les instructions entières, le 68881 exécutant les instructions flottantes. Les instructions flottantes avaient un opcode qui commençait par F (en hexadécimal), ce qui permettait de les distinguer rapidement du reste. Le 68000 chargeait les instructions, et regardait si l'instruction était destinée soit au coprocesseur, soit pour lui. Si elle était pour le coprocesseur, le 68000 lisait les opérandes depuis la mémoire et envoyait tout au coprocesseur 68881, qui exécutait l'instruction, puis rendait la main au 68000.
Les coprocesseurs Motorola utilisaient des flottants codés sur 80 bits : la mantisse était codée sur 64 bits, l'exposant sur 15 bits. Le 68881 incorporait 8 registres flottants, nommés, de 80 bits chacun. Il avait aussi un registre de statut et un registre de contrôle, guère plus. Fait étonnant, les coprocesseurs pour les PC faisaient pareil : flottants codés sur 80 bits, 8 registres flottants. Mais les détails sont différents, comme on le verra plus tard.
===Les coprocesseurs arithmétiques des consoles de jeu===
Les consoles de jeu incorporent souvent des coprocesseurs. Mais il est assez rare qu'elles incorporent des coprocesseurs arithmétiques et c'est surtout quelque chose qu'on observe sur les consoles des années 2000. La raison est que les consoles utilisent des processeurs commerciaux, utilisés dans les smartphones ou les PC. Elles n'utilisent pas de processeur spécifiquement conçu pour elles, sauf exceptions. Et de tels processeurs disposent de capacités de calcul flottant ou entières suffisantes, ils sont choisis pour. Mais il existe quelques cas où ce n'est pas le cas.
Un exemple récent est de la console de jeu Nintendo DS. La console utilisait deux processeurs, un ARM9 et un ARM7, deux processeurs RISC qui ne pouvaient pas faire de division entière. Il s'agit pourtant d'opérations importantes dans le cas du rendu 3D, ce qui fait que les concepteurs de la console ont rajouté un coprocesseur spécialisé dans les divisions entières et les racines carrées. Le coprocesseur était adressable directement par le processeur, comme peuvent l'être la RAM ou les périphériques, et était traité comme une entrée-sortie comme une autre.
Un autre exemple est celui du processeur CELL de la console de jeu PS3. Il était conçu spécifiquement pour cette console. Il intègre un cœur principal POWER PC v5 et 8 cœurs qui servent de processeurs auxiliaires. Le processeur principal est appelé le PPE et les processeurs auxiliaires sont les SPE. Les SPE sont reliés à une mémoire locale (''local store'') de 256 kibioctets qui communique avec le processeur principal via un bus spécial. Cette fois-ci, les coprocesseurs sont intégrés dans le même processeur.
Les SPE communiquent avec la RAM principale via des contrôleurs DMA. Les SPE possèdent des instructions permettant de commander leur contrôleur DMA et c'est le seul moyen qu'ils ont pour récupérer des informations depuis la mémoire. Et c'est au programmeur de gérer tout ça ! C'est le processeur principal qui va envoyer aux SPE les programmes qu'ils doivent exécuter. Il délègue des calculs aux SPE en écrivant dans le local store du SPE et en lui ordonnant l’exécution du programme qu'il vient d'écrire.
[[File:Schema Cell.png|centre|vignette|upright=2|Architecture du processeur CELL de la PS3. Le PPE est le processeur principal, les SPE sont des processeurs auxiliaires qui comprennent : un ''local store'' noté LS, un processeur noté SXU, et un contrôleur DMA pour échanger des informations avec la mémoire principale.]]
==Le jeu d'instruction x87 d'Intel==
Un exemple de coprocesseur arithmétique est celui de l'extension x87 qui a ajouté les nombres flottants aux processeurs x86. Pour rappel, les processeurs x86 sont ceux qui sont présents à l'intérieur des PC, à savoir tous les processeurs Intel et AMD actuels. Il s'agit d'un jeu d'instruction qui a reçu de nombreux ajouts au cours du temps. Les ajouts en question sont appelés des '''extensions x86'''. Nous allons maintenant voir celle qui a ajouté la gestion des flottants au x86. L'extension x87 n'est plus utilisée depuis l'arrivée des CPU 64 bits, car elle a été remplacée par l'extension SSE.
Sur les tout premiers processeurs x86, le support des nombres flottants n'était pas implémenté. A la place, ils utilisaient des coprocesseurs arithmétiques, appelés des '''coprocesseurs x87'''. Ils travaillaient en tandem avec un processeur x86 normal, et ne géraient que des instructions arithmétiques sur des flottants. Ils étaient capables d'exécuter les 4 opérations de base (add, sub, mul, div), la racine carrée, les opérations trigonométriques sinus, cosinus et tangente, l'arc tangente, et des instructions de calcul de logarithmes ou d'exponentielles.
===La pseudo-pile de registres===
Les coprocesseurs arithmétiques x87 avaient 8 registres flottants, qui étaient gérés avec une pseudo-pile, ainsi que 3 registres de contrôle. Les 8 registres x87 sont ordonnés et numérotés de 0 à 7. Les registres x87 sont organisés sous la forme d'une pile de registres, similaire à une pile d'assiette, que l'on remplit dans un ordre bien précis. Lors du chargement d'un opérande de la RAM vers les registres, il n'est pas possible de décider du registre de destination. Si on veut ajouter des flottants dans nos registres, on doit les « remplir » dans un ordre de remplissage imposé : on remplit d'abord le registre 7, puis le 6, puis le 5, et ainsi de suite jusqu'au registre 0. Si on veut ajouter un flottant dans cette pile de registres, celui-ci sera stocké dans le premier registre vide dans l'ordre de remplissage indiqué au-dessus.
Prenons un exemple, les 3 premiers registres sont occupés par un flottant et on veut charger un flottant supplémentaire : le 4e registre sera utilisé pour stocker ce flottant. La même chose existe pour le « déremplissage » des registres. Imaginez que vous souhaitez déplacer le contenu d'un registre dans la mémoire RAM et effacer complètement son contenu. On ne peut pas choisir n'importe quel registre pour faire cela : on est obligé de prendre le registre non vide ayant le numéro le plus grand.
{|
|[[File:Pseudo-pile x87. - PUSH.png|vignette|upright=2|Pseudo-pile x87 - chargement d'une opérande.]]
|[[File:Pseudo-pile x87 - POP.png|vignette|upright=2|Pseudo-pile x87 - retrait d'une opérande.]]
|}
Les instructions à un opérande dépilent le flottant au sommet de la pile. Les instructions dyadiques peuvent dépiler les deux opérandes au sommet de la pile, mais elles peuvent aussi utiliser d'autres modes d'adressage. Elles peuvent aller chercher la seconde opérande en RAM, en fournissant une adresse. Mais elles peuvent aussi adresser n'importe quel autre registre de la pile en fournissant son numéro de registre. Avec ce dernier mode d'adressage, le processeur agit comme une sorte de processeur à accumulateur, avec le sommet de la pile servant d'accumulateur.
En somme, les registres de la pile sont adressables, du moins pour ce qui est de gérer la seconde opérande. C'est cette particularité qui vaut le nom de pseudo-pile à cette organisation à mi-chemin entre une pile et une architecture à registres.
===Les instructions x87===
Maintenant, voyons quelles instructions les FPU x87 devaient gérer. L'extension x87 est en effet un jeu d'instruction, qui décrit quelles instructions doivent être gérées. Les instructions utilisent des opcodes inutilisés dans le jeu d'instruction x86, qui sont détournés pour fonctionner sur le x87.
On trouve évidemment des instructions de calculs, bien évidemment compatibles avec la norme IEE754 : l'addition FADD, la soustraction FSUB, la multiplication FMUL et la division FDIV. Mais il y a aussi la racine carrée FSQRT. La FPU x87 implémente aussi des instructions trigonométriques, qui ne sont pas supportées par la norme IEEE 754. Elle gérait l'instruction FCOS pour le cosinus, l'instruction FSIN pour le sinus, l'instruction FPTAN pour la tangente, l'instruction FPATAN pour l'arc tangente, ainsi que des instructions de calcul de logarithmes ou d'exponentielles.
La FPU x87 dispose aussi d'instructions de comparaisons compatibles avec la norme IEEE 754, capables de comparer le flottant au sommet de la pile avec un autre nombre qui peut être flottant ou entier ! Voici une liste de quelques instructions de comparaisons supportées par les FPU 87 :
* FCOM : compare le contenu du registre 0 avec une constante flottante ;
* FCOMI : compare le contenu des registres 0 et 1 ;
* FICOM : compare le contenu du registre 0 avec une constante entière ;
* FTST : compare le registre numéroté 0 avec la valeur 0.
Le x87 avait aussi des instructions de calcul de la valeur absolue (FABS) ou encore de changement de signe (FCHS).
Les instructions de calcul n'ayant besoin que d'un seul flottant pour s'exécuter, comme les opérations trigonométriques ou la valeur absolue, utilisent le flottant situé au sommet de la pile. Les instructions dyadiques (multiplication, addition, soustraction et autres) vont agir différemment suivant la situation. Elles peuvent prendre les deux flottants les plus haut placés dans cette pile, prendre le flottant au sommet de la pile, utiliser une donnée en provenance de la mémoire, ou encore utiliser le flottant le plus haut placé et un flottant stocké dans l'importe quel registre de cette pile de registres. La pile de registre était donc une sorte de mélange entre un accumulateur et 7 registres adressables.
Pour charger des opérandes dans la pile d'opérande, l'extension x87 fournit trois instructions d'accès mémoire.
{|class="wikitable"
|-
! Instruction
! Description
|-
! FLD
| Charge un nombre flottant depuis la mémoire vers notre pile de registres vue au-dessus. Cette instruction peut charger un flottant codé sur 32 bits, 64 bits ou 80 bits
|-
! FSTP
| Déplace le contenu d'un registre vers la mémoire. Une autre instruction existe qui est capable de copier le contenu d'un registre vers la mémoire sans effacer le contenu du registre : c'est l'instruction FST
|-
! FXCH
| Échange le contenu du dernier registre non vide dans l'ordre de remplissage (celui situé au sommet de la pile) avec un autre registre
|}
D'autres instructions existent qui chargent certaines constantes (PI, 1, 0, certains logarithmes en base 2) dans le registre au sommet de la pile de registres.
===Les registres de contrôle et d'état===
Pour gérer la pseudo-pile, les registres pour les flottants sont associés à un registre d'état nommé '''''Tag Word'''''. Le registre ''Tag Word'' indique, pour chaque registre flottant, s'il est vide ou non. Avouez que c'est pratique pour gérer la pile de registres vue au-dessus ! Le registre ''tag word'' fait 16 bits, ce qui fait 2 bits pour chacun des 8 registres. Ces deux bits contiennent des informations sur le contenu du registre de données réservé.
* Si ces deux bits valent 00, le registre contient un flottant « normal » différent de zéro ;
* Si ces deux bits valent 01, le registre contient une valeur nulle : 0 ;
* Si ces deux bits valent 10, le registre contient un NAN, un infini, ou un dénormal ;
* Si ces deux bits valent 11, le registre est vide et ne contient pas de nombre flottant.
Le processeur x87 contient aussi deux registres d'état, nommés ''Control Word'' et ''Status Word''. Le registre ''Status Word'' contient quelques bits, certains utilisés pour gérer la pseudo-pile, d'autres non. Il fait lui aussi 16 bits et c'est un registre d'état qui est utilisé pour qu'un programme puisse comprendre la cause d'une exception. Il contient le numéro du registre juste au-dessus du sommet de la pile, le numéro du premier registre vide dans l'ordre de remplissage. Mais il contient surtout des bits mis à 1 en cas de débordement de flottant, de division par zéro, lorsqu'un calcul a pour résultat un dénormal, etc.
{|class="wikitable"
|-
! Bit !! Utilité
|-
! TOP
| Trois bits, qui codent le numéro du premier registre vide dans la pile de registre
|-
! U
| Détecte les ''underflows'' : est mis à 1 en cas d'''underflows''.
|-
! O
| | Détecte les ''overflows'' : est mis à 1 en cas d'''overflows''.
|-
! Z
| Prévient qu'une division par zéro a eu lieu. Est mis à 1 si c'est le cas.
|-
! D
| Bit est mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal
|-
! I
| Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0
|}
Le registre ''Control Word'' fait 16 bits et configure la gestion des arrondis.
{|class="wikitable"
|-
! Bit !! Utilité
|-
! Infinity Control
| S'il vaut zéro, les infinis sont tous traités comme s'ils valaient +∞. S'il vaut un, les infinis sont traités normalement
|-
! Rouding Control
| C'est un ensemble de deux bits qui détermine le mode d'arrondi utilisé
* 00 : vers le nombre flottant le plus proche : c'est la valeur par défaut ;
* 01 : vers - l'infini ;
* 10 : vers + l'infini ;
* 11 : vers zéro
|-
! Precision Control
|`Ensemble de deux bits qui détermine la taille de la mantisse de l'arrondi du résultat d'un calcul. En effet, on peut demander à notre FPU d'arrondir le résultat de chaque calcul qu'elle effectue. Cette instruction ne touche pas à l'exposant, mais seulement à la mantisse. La valeur par défaut de ces deux bits est 11 : notre FPU utilise donc des flottants double précision étendue. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754.
* 00 : mantisse codée sur 24 bits ;
* 01 : valeur inutilisée ;
* 10 : mantisse codée sur 53 bits ;
* 11 : mantisse codée sur 64 bits
|}
Les instructions x87 sont codées sur au minimum deux octets. Le premier octet commence toujours la suite de bit 11011, qui indique que c'est une instruction destinée au coprocesseur. Le 11011 était appelé le code d'échappement, sa mnémonique en assembleur était ESC. Le tout est suivi par 6 bits d'opcode, et 5 bits pour le mode d'adressage. Le tout était regroupé comme suit : 1101 1xxx aaxx xaaa, avec x les bits de l'opcode et a pour le mode d'adressage. Le tout était suivi par des opérandes, selon le mode d'adressage.
==Les coprocesseurs x87 et leurs précurseurs==
Les premiers coprocesseurs de ce type étaient l'Intel 8231/8232, destinés à être utilisés avec le 8088, un processeur 8 bits. Par la suite, Intel a récidivé avec le 8087, qui était destiné pour servir en tandem avec le 8086. Il a été suivi par les Intel 187, le 287, le 387, le 487 et le 587, qui étaient censés servir avec les CPU 186, 286, 386, 486, etc. Mais d'autres compagnies ont crée des coprocesseurs x87, comme Weitek, Cyrix, AMD, Texas Instrument, et bien d'autres.
===Les précurseurs : l'Intel 8231 et le 8232===
L'Am9511 et Am9512 sont une des toutes premières FPU pour PC, si ce n'est les premières si on en croit AMD. Ils ont été licenciés par Intel sous le nom d'Intel 8231 et le 8232. Ils étaient conçus pour complémenter le CPU Intel 8080, mais on pouvait parfaitement les utiliser avec d'autres CPU, comme le Z80. La [http://ep.homeserver.hu/PDF/AM9511A-9512.pdf documentation AMD donnait même des exemples assez variés]. La raison est qu'on y accédait comme n'importe quelle entrée-sortie connectée au bus système. Ils étaient accessibles via ''pooling'', interruptions ou même via DMA. Nous expliquerons comment c'est possible plus bas.
L'Intel 8231 ne supportait pas le jeu d'extension x87, qui est apparu après. Il gérait des nombres flottants de 32 bits, mais aussi des nombres en virgule fixe de 16 et 32 bits. Les flottants 32 bits suivaient globalement la norme IEEE 754, mais les nombres en virgule fixe utilisaient un format propriétaire. Il gérait les quatre opérations de base, mais aussi des calculs trigonométriques. Il utilisait pour cela du microcode, avec une approximation basée sur des polynômes de Tchebychev. L'intel 8232 supportait lui des flottants 32 et 64 bits, mais ne supportait que les quatre opérations de base (addition, soustraction, multiplication et division).
: La documentation décrit ces flottants 32 bits comme étant de la double précision, mais c'est parce que la terminologie de l'époque n'était pas encore bien stabilisée.
[[File:C8231A FPU PIN CONFIGURATION.png|thumb|Intel 8231 FPU PIN CONFIGURATION.]]
Les broches de l'intel 8231 sont illustrées ci-contre. La plupart des broches nous sont familières : 8 broches pour le bus de données (qui fait 8 bits), une entrée d'horloge, une entrée de RESET, une entrée ''chip select'' pour le décodage d'adresse. Les broches restantes sont très intéressantes, mais on les verra dans ce qui suit.
Toujours est-il que le coprocesseur est relié à un bus de 8 bits, alors que ses registres font 32 à 64 bits. Pour cela, le 8231/8232 lisait les opérandes octet par octet depuis le bus de données. Idem mais pour les écritures. Par contre, les instructions sont prévues pour faire 8 bits, pas plus. Pour avoir des instructions aussi courtes, la seule solution est d'utiliser une machine à pile et c'est ce que le 8231/8232 a fait. Précisons cependant que ce n'est pas la même pile de registre que la pile x87, mais c'était une sorte de pile similaire, qui a évolué pour donner la pile x87. Il s'agissait pour le coup d'une vraie pile, les opérations utilisaient systématiquement le sommet de la pile et l'opérande en dessous. Il n'y avait pas de possibilité d'adresser un opérande dans la pile.
Le processeur intégrait 8 registres de 16 bits, organisés comme une pile. Les registres pouvaient être utilisés : soit comme une pile de 8 opérandes 16 bits, soit une pile de 4 opérandes 32 bits, soit une pile de 2 opérandes 64 bits. Les opérandes étaient empilées octet par octet dans le processeur. Ils étaient dépilés là aussi octet par octet. Pour cela, le 8231/8232 dispose de trois entrées nommées A0, RD et WR. Les trois bits décident s'il faut faire une lecture, une écriture, exécuter une instruction, ou lire le registre d'état. Les quatre opérations sont appelées des commandes dans la documentation Intel et AMD. Les trois entrées font donc office de bus de commande simplifié.
{|class="wikitable"
|-
! A0, RD, WR !! Action
|-
| 000 || Lecture de l'octet depuis les registres de données.
|-
| 010 || Écriture de l'octet dans les registres de données.
|-
| 111 || L'octet est l'opcode d'une instruction, qui est exécutée immédiatement.
|-
| 101 || Lecture du registre d'état.
|}
Le processeur principal envoyait des commandes à l'Intel 8231/8232, qui les exécutait dans son coin. Le 8231/8232 envoyait un signal END OF EXECUTION pour prévenir qu'il avait fini son travail, que la commande précédente était terminée. Il avait une broche dédiée, appelée END, dédiée à ça. Le coprocesseur avait donc une interface de communication asynchrone, qui se voit quand on étudie ses broches. Les broches suivantes servent à la communication asynchrone avec le 8231/8232.
* READY est à 1 quand le 8231/8232 est libre, capable d'accepter une nouvelle instruction/commande. Il passe à 0 quand une instruction démarre, avec la commande 111 vue plus haut.
* END indique que la commande précédente a terminé son exécution. Lorsque END passe à 1, BUSY passe automatiquement à 0.
* EACK est une entrée sur laquelle le processeur dit qu'il a bien reçu le signal END, et que ce dernier peut être remis à 0.
Ce système pouvait être utilisé avec du ''pooling'', avec des interruptions, voire du DMA. Avec des interruptions, la sortie END était utilisée comme sortie d'interruption, reliée au CPU ou au contrôleur d'interruption. Pour le ''pooling'', le registre d'état du 8231/8232 contenait un bit BUSY, qui indiquait si le coprocesseur était utilisé ou non.
Un tel fonctionnement peut sembler étrange, et vous aurez l'impression que communiquer avec le coprocesseur est très lent. Mais cela prend tout son sens quand on connait le temps mis pour exécuter une instruction sur le coprocesseur. Une opération simple sur des flottants 32 bits prenait facilement une cinquantaine de cycles d'horloge, et c'était parmi les meilleurs temps de calcul. Il n'était pas rare d'avoir des opérations prenant plusieurs centaines, voire milliers de cycles d'horloge. Pas loin de 5000 cycles d'horloge pour une division de deux flottants 64 bits sur le 8232, plusieurs dizaines de milliers de cycles pour certaines opérations trigonométriques. Et le pire, c'était que c'était plus rapide que l'émulation logicielle !
Pas étonnant donc que le 8231/8232 aient été traités comme des entrées-sorties, à une époque ou tout était connecté sur un bus système assez rapide. Un autre avantage est que le 8231/8232 pouvaient fonctionner à une fréquence sans rapport avec celle du processeur. Par exemple, on pouvait utiliser un processeur à 1 MHz alors que le 8231/8232 allait à 4 MHz. Le coprocesseur faisait juste des calculs rapidement, comparé au CPU. Et ça a été utilisé sur certains systèmes Apple II. Ou encore, on pouvait utiliser un processeur légèrement plus rapide que le coprocesseur, avec quelques MHz de différence, comme un CPU à 5 MHz avec un coprocesseur de 2 MHz.
===L'intel 8087 et ses successeurs===
[[File:Intel 8087.svg|vignette|Intel 8087]]
Le 8087 été fabriqué avec 65 000 transistors. Le 8087 avait pour particularité qu'il était connecté directement sur le bus mémoire, au même titre que le 8086. Mais le 8087 n'avait pas de bus d'adresse et de données séparé. Le processeur utilisait un bus multiplexé. Il avait 20 broches pour se connecter au bus : 16 d'entre elles servaient alternativement de bus d'adresse et de données. L'interface avec le bus était donc un peu compliquée.
L'intel 387 était le coprocesseur associé au 386 d'Intel. Il était le premier coprocesseur à s'intégrer sur un bus de 32 bits. Il a été décliné en plusieurs versions, dont certaines sont spécifiques à un modèle de 386. Par exemple, le i386SX était une version simplifiée du 386 initial, qui avait notamment un bus de seulement 16 bits. Et de ce fait, il avait son propre coprocesseur i387SX, qui était adapté à un bus de 16 bits. De même, le i386SL était adapté aux ordinateurs portables et avait son propre coprocesseur i387SL.
Tout ce qui va suivre est valide pour tous les coprocesseurs x87 de marque Intel.
Le processeur central lisait des instructions, en envoyant le ''program counter'' sur le bus d'adresse, les instructions étaient récupérées sur le bus de données. Là, les deux processeurs déterminaient si l'instruction chargée était destinée au coprocesseur ou au CPU. Pour cela, les instructions x87 commencent toutes par la suite de bit 11011, qui permet de savoir facilement si une instruction est destinée au coprocesseur. Le 11011 était suivi par un opcode et un mode d'adressage.
Si le mode d'adressage demandait de lire un opérande mémoire, le 8086 envoyait l'adresse de l'opérande sur le bus, et le coprocesseur récupérait celle-ci sur le bus de données. Si l'opérande devait être lu en plusieurs fois, le coprocesseur lisait le reste de lui-même, en prenant le contrôle du bus d'adresse. Il récupérait l'adresse envoyée initialement par le CPU, puis l'incrémentait et relançait un nouvel accès mémoire. Il l'incrémentait autant de fois que nécessaire pour charger l'opérande.
Un problème est que le CPU ne sait pas combien de temps dure une instruction x87. Et cette durée dépendait de l'implémentation du processeur, elle n'était pas la même selon la marque du coprocesseur. Un 186 n'avait pas les mêmes timings que le 286, par exemple. Pour le CPU, une instruction x87 met juste deux cycles pour s'exécuter (plus si des opérandes doivent être lus en mémoire). Pour cela, le CPU disposait d'un mécanisme de synchronisation.
Le mécanisme de synchronisation était une instruction WAIT, qui forçait le CPU à attendre que le coprocesseur ait terminé l'instruction précédente. L'implémentation matérielle était assez simple. Le coprocesseur disposait d'une sortie BUSY, qui indiquait qu'il était en train d'exécuter une instruction et ne pouvait pas en accepter une nouvelle. Le CPU, quant à lui, avait une entrée TEST qui vérifiait si le, coprocesseur était occupé ou non. La sortie BUSY était reliée à l'entrée TEST. L'instruction test vérifiait juste ce qu'il y avait sur l'entrée TEST. Tant qu'elle était à 1, le processeur attendait et ne chargeait pas de nouvelle instruction. Dès qu'elle passe à 0, l'exécution reprend.
Il faut noter que l'instruction WAIT n'est nécessaire qu'entre deux instructions flottantes assez proches. Mais il est possible d'intercaler des instructions entières entre deux instructions flottantes. Le programme pouvait ainsi mixer instructions entières et flottantes, les instructions entières étant exécutées sur le 8086, les instructions flottantes sur le coprocesseur. Il y avait donc une possibilité de parallélisme, à savoir que les deux processeurs pouvaient exécuter des instructions différentes en même temps. Mais cela demandait que les calculs soient coopératifs et mélangent bien entiers et flottants.
Le 8087 et ses successeurs avaient une microarchitecture assez simple. L'unité de contrôle contenait un décodeur d'instruction microcodé, le registre de contrôle, le registre d'état. La plupart des instructions sont microcodées, l'unité de calcul est assez limitée. Elle permet d'additionner deux mantisses flottantes, de faire des décalages, d'additionner deux exposants, mais pas plus. Les multiplications et divisions sont donc microcodées et émulées en enchainant des additions flottantes. Les instructions trigonométriques sont implémentées en utilisant l'algorithme CORDIC, qu'on a vu dans le chapitre sur les circuits de calcul flottant.
[[File:Intel 8087 arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel 8087.]]
Le chemin de données est composé d'un banc de registre flottant pour la pseudo-pile, et de plusieurs circuits de calcul. Le banc de registre était mono-port, ce qui fait que les ALUs étaient précédés par deux registres temporaires pour les opérandes. Les circuits pour l'exposant et la mantisse sont séparés, et sont même reliés au banc de registre par deux bus séparés. Il y a un additionneur pour les exposants, un additionneur pour les mantisses et un décaleur pour les mantisses (pour les normaliser).
De plus, on trouve une mémoire ROM dédiée aux constantes les plus utilisées. Elle sert pour les constantes de base, gérées par le jeu d'extension x87. Mais elle contient aussi des constantes utilisées pour l'algorithme CORDIC. Elle n'est pas illustrée sur le schéma ci-dessous, mais elle existe.
L'interface avec le bus est un simple registre d’interfaçage avec le bus. Pour rappel, le bus de données fait 16 bits sur le 8087, 32 bits sur le 387. Entre le bus et le chemin de données, on trouve une file servant à simplifier la gestion des lectures. L'idée est que les opérandes lus/écrits font 32, 64 ou 80 bits, alors que le bus de données n'en fait que 16/32. Les opérandes sont donc lus/écrits en plusieurs passes. Sur le 8087, il doit réaliser deux passes pour des opérandes de 32 bits, quatre passes pour celles de 64 bits, 5 pour des opérandes de 80 bits (80 = 5 × 16). Le 387 doit faire deux fois moins.
[[File:Intel 387 arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel 387. Les circuits pour les exposants sont à gauche dans le chemin de données, les circuits pour les mantisses sont à droite.]]
L'implémentation du banc de registre est assez simple : une RAM avec un registre qui indique la position du sommet de la pile dedans. Le registre fait 3 bits, pour 8 registres. En plus de cela, il y a un petit soustracteur et un multiplexeur, pour adresser les opérandes dans la pile. Pour rappel, il est possible d'adresser la seconde opérande dans la pile. Mais on précise pas le numéro du registre dans la pile pour cela, on précise sa position sous le sommet de la pile, à savoir si elle est deux, trois, quatre opérandes sous le sommet de la pile. Pour déterminer quel registre lire, il faut soustraire ce "décalage" au numéro de registre du sommet de la pile. Pour cela, il y a un petit soustracteur pour faire le calcul.
Le circuit décaleur est composé de deux sous-décaleurs. Le premier fait des décalages au niveau des octets, le second décale l'opérande de 0 à 7 rangs.
Pour finir, voici quelques liens sur la microarchitecture du 8087 :
* [https://www.righto.com/2026/02/8087-instruction-decoding.html Instruction decoding in the Intel 8087 floating-point chip].
* [https://www.righto.com/2020/05/die-analysis-of-8087-math-coprocessors.html Die analysis of the 8087 math coprocessor's fast bit shifter].
* [https://www.righto.com/2025/12/8087-stack-circuitry.html The stack circuitry of the Intel 8087 floating point chip, reverse-engineered].
* [https://www.righto.com/2025/12/8087-microcode-conditions.html Conditions in the Intel 8087 floating-point chip's microcode].
* [https://www.righto.com/2020/05/extracting-rom-constants-from-8087-math.html Extracting ROM constants from the 8087 math coprocessor's die].
* [https://www.righto.com/2018/09/two-bits-per-transistor-high-density.html Two bits per transistor: high-density ROM in Intel's 8087 floating point chip].
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les sections critiques et le modèle mémoire
| prevText=Les sections critiques et le modèle mémoire
| next=L'accélération matérielle de la virtualisation
| nextText=L'accélération matérielle de la virtualisation
}}
</noinclude>
i0ajgpvljtxv5m09uozau8jswo8hxa2
763008
763007
2026-04-05T18:42:45Z
Mewtow
31375
/* Les coprocesseurs arithmétiques : généralités et exemples */
763008
wikitext
text/x-wiki
Les processeurs actuels sont des processeurs multicœurs, à savoir qu'ils intègrent plusieurs processeurs dans un seul circuit intégré. Du moins, c'est une explication simplifiée, des circuits sont mutualisés entre les processeurs. Les cœurs sont tous identiques, à savoir qu'ils ont le même jeu d'instruction, ont une microarchitecture similaire, etc. Mais ce n'est possible que parce que les processeurs actuels ont un grand nombre de transistors, grâce à la loi de Moore. Pour intégrer plusieurs cœurs sur un processeur, il faut le budget en transistor pour. Mais au siècle dernier, le budget en transistors n'était pas là. Aussi, utiliser plusieurs processeurs était plus compliqué.
Les ordinateurs de l'époque pouvaient utiliser plusieurs processeurs identiques, mais il fallait que la carte mère le supporte. Et les cartes mères avec plusieurs sockets identiques étaient rares. Les cartes mères pour serveur en étaient capables, mais pas les cartes mères grand public. Les cartes mères plus grand public avaient cependant une option pour gérer plusieurs processeurs. Quelques ordinateurs assez anciens disposaient de '''coprocesseurs''', des processeurs secondaires qui complémentaient un processeur principal. Nous en avions déjà vu dans le chapitre sur l'architecture de base, et avions vu quelques exemples, provenant tous d'anciennes consoles de jeu. Mais il est temps de voir ces coprocesseurs en détail
==Les différents types de coprocesseurs : son, IO, FPU==
Le CPU et le coprocesseur sont foncièrement différents : ils n'ont pas le même jeu d'instruction, n'ont pas le même rôle, et j'en passe. Sauf exception, le CPU et le coprocesseur exécutent des programmes différents, ils travaillent en parallèle. Le coprocesseur a donc un rôle secondaire, reste à voir à quoi il sert.
===Les différents types de coprocesseurs===
Les coprocesseurs peuvent se classer en plusieurs catégories : les coprocesseurs sonores, arithmétiques, et d'entrées-sorties.
Les '''coprocesseurs sonores''' sont une sorte d'ancêtre des cartes son, utilisés sur les anciennes consoles de jeux vidéo, comme La Nintendo 64, la Playstation et autres consoles antérieures. Ils s'occupaient respectivement de calculer tout ce qui a trait au son. Pour donner un exemple, on peut citer la console Neo-géo, qui disposait de deux processeurs travaillant en parallèle : un processeur principal, et un coprocesseur sonore. Le processeur principal était un Motorola 68000, alors que le coprocesseur sonore était un processeur Z80.
Les '''coprocesseur d'IO''' sont dédiés à l'accès aux entrées-sorties. Pour simplifier, ce sont des contrôleurs DMA programmables, capables d'effectuer quelques opérations de branchements et de calcul. Nous en avions parlé dans le chapitre sur les entrée-sorties, je ne reviendrais pas dessus ici.
[[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]]
Les '''coprocesseurs arithmétiques''' sont dédiés aux calculs en virgule flottante. Ils étaient utilisés à une époque où les CPU ne géraient que des calculs entiers (en binaire ou en BCD). Un exemple est le coprocesseur flottant x87, complémentaire des premiers processeurs Intel x86. Il y a eu la même chose sur les processeurs Motorola 68000, avec deux coprocesseurs flottants appelés les Motorola 68881 et les Motorola 68882. Ils sont aujourd'hui tombés en désuétude, depuis que les CPU sont devenus capables de faire des calculs sur des nombres flottants.
===Les coprocesseurs faiblement et fortement couplés===
Un coprocesseur peut être un processeur fait sur mesure, qui ne peut que servir de processeur secondaire, comme le sont tous les coprocesseurs arithmétiques. À l'inverse, de nombreuses consoles de jeu utilisaient un processeur des plus banals comme coprocesseur. Par exemple, de nombreuses consoles avaient un processeur principal, complété par un coprocesseur Z80 en guise de carte son. Et cette distinction est très liée à celle qui distingue les coprocesseurs faiblement couplés, et fortement couplés.
Le cas le plus fréquent est celui où le CPU et le coprocesseur travaillent en parallèle et exécutent des programmes différents. On parle alors de '''coprocesseurs faiblement couplés'''. Les coprocesseurs sonores et d'IO sont tous dans ce cas. Le coprocesseur sonore exécute un programme pour gérer le son, qui est séparé au programme principal. Le programme principal communique avec le coprocesseur, mais c'est assez rare. Dans un jeu vidéo, cela arrive seulement quand il faut changer de musique ou déclencher un effet sonore.
Les deux travaillent en parallèle, et il existe des mécanismes de synchronisation entre les deux processeurs. Typiquement, la synchronisation se fait en utilisant des ''interruptions inter-processeurs'', à savoir qu'un processeur exécute une interruption qui est exécutée par l'autre processeur. L'implémentation demande d'ajouter des circuits pour gérer ces interruptions entre processeurs.
Un exemple est celui des consoles néo-géo et Megadrive. Elles intègrent deux processeurs : un Motorola 68000 qui sert de processeur principal, un Z80 qui sert de processeur dédié à l'audio. Le MC68000 envoie des commandes au Z80, mais la communication ne va pas dans l'autre sens. Les deux processeurs communiquent via l'intermédiaire d'un ''IO arbiter chip'', qui gère les interruptions inter-processeur. Il contient un registre de 8 bits, dans lequel le MC68000 peut écrire un numéro d'interruption, qui indiquent quelle routine d'interruption exécuter. Lorsque le MC68000 écrit une valeur dedans, cela déclenche l’exécution automatique d'une interruption sur le Z80.
[[File:Architecture de la Megadrive et de la Néogeo.png|centre|vignette|upright=2.5|Architecture de la Megadrive et de la Néogeo]]
Les coprocesseurs arithmétiques se distinguent des autres, car ils fonctionnent en tandem avec le processeur principal, pas en parallèle. Les coprocesseurs précédents sont autonomes, à savoir qu'ils exécutent un programme différent de celui exécuté par le CPU. Mais les coprocesseurs arithmétiques ne sont pas dans ce cas. Il n'y a qu'un seul programme à exécuter, qui contient des instructions à destination du CPU, d'autres à destination du coprocesseur. Les instructions sont exécutées soit par le CPU, soit par le coprocesseur, une par une. En clair, le CPU et le coprocesseur se passent à la main à tour de rôle, ils ne travaillent pas en parallèle. On parle alors de '''coprocesseurs fortement couplés'''.
==Les coprocesseurs arithmétiques : généralités et exemples==
Dans le reste de ce chapitre, nous allons surtout voir les coprocesseurs arithmétiques. Et ce pour une raison très simple : nous avons déjà vu les coprocesseurs I/O et les coprocesseurs sonores dans un chapitre antérieur. Pour rappel, les coprocesseurs sonores sont des cartes sons, ou du moins une partie de carte son. De plus, on a plus de documentation sur les coprocesseurs arithmétiques. il faut dire que c'étaient des processeurs commerciaux, vendus autrefois en magasin, avec de la documentation destinée aux utilisateurs.
===Les anciens co-processeurs flottants des PCs===
Les coprocesseurs arithmétiques étaient spécialisés dans les calculs flottants et étaient généralement fortement couplés. Les coprocesseurs étaient optionnels et il était parfaitement possible de monter un PC qui n'en avait pas. En conséquence, les programmeurs devaient coder des programmes qui peuvent fonctionner avec et sans coprocesseur. La solution la plus simple était de fournir deux versions du logiciel : une sans usage du coprocesseur, et une autre qui en fait usage, plus rapide. Une autre solution était d'émuler les calculs flottants en logiciel.
Un bon exemple de la seconde solution est le 68881 de Motorola, conçu pour fonctionner avec les CPU 6802 et 68030. Les programmes mixaient instructions entières et flottantes, le 68000 exécutant les instructions entières, le 68881 exécutant les instructions flottantes. Les instructions flottantes avaient un opcode qui commençait par F (en hexadécimal), ce qui permettait de les distinguer rapidement du reste. Le 68000 chargeait les instructions, et regardait si l'instruction était destinée soit au coprocesseur, soit pour lui. Si elle était pour le coprocesseur, le 68000 lisait les opérandes depuis la mémoire et envoyait tout au coprocesseur 68881, qui exécutait l'instruction, puis rendait la main au 68000.
Les coprocesseurs Motorola utilisaient des flottants codés sur 80 bits : la mantisse était codée sur 64 bits, l'exposant sur 15 bits. Le 68881 incorporait 8 registres flottants, nommés, de 80 bits chacun. Il avait aussi un registre de statut et un registre de contrôle, guère plus. Fait étonnant, les coprocesseurs pour les PC faisaient pareil : flottants codés sur 80 bits, 8 registres flottants. Mais les détails sont différents, comme on le verra plus tard.
===Les coprocesseurs arithmétiques des consoles de jeu===
Les consoles de jeu incorporent souvent des coprocesseurs. Mais il est assez rare qu'elles incorporent des coprocesseurs arithmétiques et c'est surtout quelque chose qu'on observe sur les consoles des années 2000. La raison est que les consoles utilisent des processeurs commerciaux, utilisés dans les smartphones ou les PC. Elles n'utilisent pas de processeur spécifiquement conçu pour elles, sauf exceptions. Et de tels processeurs disposent de capacités de calcul flottant ou entières suffisantes, ils sont choisis pour. Mais il existe quelques cas où ce n'est pas le cas.
Un exemple récent est de la console de jeu Nintendo DS. La console utilisait deux processeurs, un ARM9 et un ARM7, deux processeurs RISC qui ne pouvaient pas faire de division entière. Il s'agit pourtant d'opérations importantes dans le cas du rendu 3D, ce qui fait que les concepteurs de la console ont rajouté un coprocesseur spécialisé dans les divisions entières et les racines carrées. Le coprocesseur était adressable directement par le processeur, comme peuvent l'être la RAM ou les périphériques, et était traité comme une entrée-sortie comme une autre.
Un autre exemple est celui du processeur CELL de la console de jeu PS3. Il était conçu spécifiquement pour cette console. Il intègre un cœur principal POWER PC v5 et 8 cœurs qui servent de processeurs auxiliaires. Le processeur principal est appelé le PPE et les processeurs auxiliaires sont les SPE. Les SPE sont reliés à une mémoire locale (''local store'') de 256 kibioctets qui communique avec le processeur principal via un bus spécial. Cette fois-ci, les coprocesseurs sont intégrés dans le même processeur.
Les SPE communiquent avec la RAM principale via des contrôleurs DMA. Les SPE possèdent des instructions permettant de commander leur contrôleur DMA et c'est le seul moyen qu'ils ont pour récupérer des informations depuis la mémoire. Et c'est au programmeur de gérer tout ça ! C'est le processeur principal qui va envoyer aux SPE les programmes qu'ils doivent exécuter. Il délègue des calculs aux SPE en écrivant dans le local store du SPE et en lui ordonnant l’exécution du programme qu'il vient d'écrire.
[[File:Schema Cell.png|centre|vignette|upright=2|Architecture du processeur CELL de la PS3. Le PPE est le processeur principal, les SPE sont des processeurs auxiliaires qui comprennent : un ''local store'' noté LS, un processeur noté SXU, et un contrôleur DMA pour échanger des informations avec la mémoire principale.]]
==Le jeu d'instruction x87 d'Intel==
Un exemple de coprocesseur arithmétique est celui de l'extension x87 qui a ajouté les nombres flottants aux processeurs x86. Pour rappel, les processeurs x86 sont ceux qui sont présents à l'intérieur des PC, à savoir tous les processeurs Intel et AMD actuels. Il s'agit d'un jeu d'instruction qui a reçu de nombreux ajouts au cours du temps. Les ajouts en question sont appelés des '''extensions x86'''. Nous allons maintenant voir celle qui a ajouté la gestion des flottants au x86. L'extension x87 n'est plus utilisée depuis l'arrivée des CPU 64 bits, car elle a été remplacée par l'extension SSE.
Sur les tout premiers processeurs x86, le support des nombres flottants n'était pas implémenté. A la place, ils utilisaient des coprocesseurs arithmétiques, appelés des '''coprocesseurs x87'''. Ils travaillaient en tandem avec un processeur x86 normal, et ne géraient que des instructions arithmétiques sur des flottants. Ils étaient capables d'exécuter les 4 opérations de base (add, sub, mul, div), la racine carrée, les opérations trigonométriques sinus, cosinus et tangente, l'arc tangente, et des instructions de calcul de logarithmes ou d'exponentielles.
===La pseudo-pile de registres===
Les coprocesseurs arithmétiques x87 avaient 8 registres flottants, qui étaient gérés avec une pseudo-pile, ainsi que 3 registres de contrôle. Les 8 registres x87 sont ordonnés et numérotés de 0 à 7. Les registres x87 sont organisés sous la forme d'une pile de registres, similaire à une pile d'assiette, que l'on remplit dans un ordre bien précis. Lors du chargement d'un opérande de la RAM vers les registres, il n'est pas possible de décider du registre de destination. Si on veut ajouter des flottants dans nos registres, on doit les « remplir » dans un ordre de remplissage imposé : on remplit d'abord le registre 7, puis le 6, puis le 5, et ainsi de suite jusqu'au registre 0. Si on veut ajouter un flottant dans cette pile de registres, celui-ci sera stocké dans le premier registre vide dans l'ordre de remplissage indiqué au-dessus.
Prenons un exemple, les 3 premiers registres sont occupés par un flottant et on veut charger un flottant supplémentaire : le 4e registre sera utilisé pour stocker ce flottant. La même chose existe pour le « déremplissage » des registres. Imaginez que vous souhaitez déplacer le contenu d'un registre dans la mémoire RAM et effacer complètement son contenu. On ne peut pas choisir n'importe quel registre pour faire cela : on est obligé de prendre le registre non vide ayant le numéro le plus grand.
{|
|[[File:Pseudo-pile x87. - PUSH.png|vignette|upright=2|Pseudo-pile x87 - chargement d'une opérande.]]
|[[File:Pseudo-pile x87 - POP.png|vignette|upright=2|Pseudo-pile x87 - retrait d'une opérande.]]
|}
Les instructions à un opérande dépilent le flottant au sommet de la pile. Les instructions dyadiques peuvent dépiler les deux opérandes au sommet de la pile, mais elles peuvent aussi utiliser d'autres modes d'adressage. Elles peuvent aller chercher la seconde opérande en RAM, en fournissant une adresse. Mais elles peuvent aussi adresser n'importe quel autre registre de la pile en fournissant son numéro de registre. Avec ce dernier mode d'adressage, le processeur agit comme une sorte de processeur à accumulateur, avec le sommet de la pile servant d'accumulateur.
En somme, les registres de la pile sont adressables, du moins pour ce qui est de gérer la seconde opérande. C'est cette particularité qui vaut le nom de pseudo-pile à cette organisation à mi-chemin entre une pile et une architecture à registres.
===Les instructions x87===
Maintenant, voyons quelles instructions les FPU x87 devaient gérer. L'extension x87 est en effet un jeu d'instruction, qui décrit quelles instructions doivent être gérées. Les instructions utilisent des opcodes inutilisés dans le jeu d'instruction x86, qui sont détournés pour fonctionner sur le x87.
On trouve évidemment des instructions de calculs, bien évidemment compatibles avec la norme IEE754 : l'addition FADD, la soustraction FSUB, la multiplication FMUL et la division FDIV. Mais il y a aussi la racine carrée FSQRT. La FPU x87 implémente aussi des instructions trigonométriques, qui ne sont pas supportées par la norme IEEE 754. Elle gérait l'instruction FCOS pour le cosinus, l'instruction FSIN pour le sinus, l'instruction FPTAN pour la tangente, l'instruction FPATAN pour l'arc tangente, ainsi que des instructions de calcul de logarithmes ou d'exponentielles.
La FPU x87 dispose aussi d'instructions de comparaisons compatibles avec la norme IEEE 754, capables de comparer le flottant au sommet de la pile avec un autre nombre qui peut être flottant ou entier ! Voici une liste de quelques instructions de comparaisons supportées par les FPU 87 :
* FCOM : compare le contenu du registre 0 avec une constante flottante ;
* FCOMI : compare le contenu des registres 0 et 1 ;
* FICOM : compare le contenu du registre 0 avec une constante entière ;
* FTST : compare le registre numéroté 0 avec la valeur 0.
Le x87 avait aussi des instructions de calcul de la valeur absolue (FABS) ou encore de changement de signe (FCHS).
Les instructions de calcul n'ayant besoin que d'un seul flottant pour s'exécuter, comme les opérations trigonométriques ou la valeur absolue, utilisent le flottant situé au sommet de la pile. Les instructions dyadiques (multiplication, addition, soustraction et autres) vont agir différemment suivant la situation. Elles peuvent prendre les deux flottants les plus haut placés dans cette pile, prendre le flottant au sommet de la pile, utiliser une donnée en provenance de la mémoire, ou encore utiliser le flottant le plus haut placé et un flottant stocké dans l'importe quel registre de cette pile de registres. La pile de registre était donc une sorte de mélange entre un accumulateur et 7 registres adressables.
Pour charger des opérandes dans la pile d'opérande, l'extension x87 fournit trois instructions d'accès mémoire.
{|class="wikitable"
|-
! Instruction
! Description
|-
! FLD
| Charge un nombre flottant depuis la mémoire vers notre pile de registres vue au-dessus. Cette instruction peut charger un flottant codé sur 32 bits, 64 bits ou 80 bits
|-
! FSTP
| Déplace le contenu d'un registre vers la mémoire. Une autre instruction existe qui est capable de copier le contenu d'un registre vers la mémoire sans effacer le contenu du registre : c'est l'instruction FST
|-
! FXCH
| Échange le contenu du dernier registre non vide dans l'ordre de remplissage (celui situé au sommet de la pile) avec un autre registre
|}
D'autres instructions existent qui chargent certaines constantes (PI, 1, 0, certains logarithmes en base 2) dans le registre au sommet de la pile de registres.
===Les registres de contrôle et d'état===
Pour gérer la pseudo-pile, les registres pour les flottants sont associés à un registre d'état nommé '''''Tag Word'''''. Le registre ''Tag Word'' indique, pour chaque registre flottant, s'il est vide ou non. Avouez que c'est pratique pour gérer la pile de registres vue au-dessus ! Le registre ''tag word'' fait 16 bits, ce qui fait 2 bits pour chacun des 8 registres. Ces deux bits contiennent des informations sur le contenu du registre de données réservé.
* Si ces deux bits valent 00, le registre contient un flottant « normal » différent de zéro ;
* Si ces deux bits valent 01, le registre contient une valeur nulle : 0 ;
* Si ces deux bits valent 10, le registre contient un NAN, un infini, ou un dénormal ;
* Si ces deux bits valent 11, le registre est vide et ne contient pas de nombre flottant.
Le processeur x87 contient aussi deux registres d'état, nommés ''Control Word'' et ''Status Word''. Le registre ''Status Word'' contient quelques bits, certains utilisés pour gérer la pseudo-pile, d'autres non. Il fait lui aussi 16 bits et c'est un registre d'état qui est utilisé pour qu'un programme puisse comprendre la cause d'une exception. Il contient le numéro du registre juste au-dessus du sommet de la pile, le numéro du premier registre vide dans l'ordre de remplissage. Mais il contient surtout des bits mis à 1 en cas de débordement de flottant, de division par zéro, lorsqu'un calcul a pour résultat un dénormal, etc.
{|class="wikitable"
|-
! Bit !! Utilité
|-
! TOP
| Trois bits, qui codent le numéro du premier registre vide dans la pile de registre
|-
! U
| Détecte les ''underflows'' : est mis à 1 en cas d'''underflows''.
|-
! O
| | Détecte les ''overflows'' : est mis à 1 en cas d'''overflows''.
|-
! Z
| Prévient qu'une division par zéro a eu lieu. Est mis à 1 si c'est le cas.
|-
! D
| Bit est mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal
|-
! I
| Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0
|}
Le registre ''Control Word'' fait 16 bits et configure la gestion des arrondis.
{|class="wikitable"
|-
! Bit !! Utilité
|-
! Infinity Control
| S'il vaut zéro, les infinis sont tous traités comme s'ils valaient +∞. S'il vaut un, les infinis sont traités normalement
|-
! Rouding Control
| C'est un ensemble de deux bits qui détermine le mode d'arrondi utilisé
* 00 : vers le nombre flottant le plus proche : c'est la valeur par défaut ;
* 01 : vers - l'infini ;
* 10 : vers + l'infini ;
* 11 : vers zéro
|-
! Precision Control
|`Ensemble de deux bits qui détermine la taille de la mantisse de l'arrondi du résultat d'un calcul. En effet, on peut demander à notre FPU d'arrondir le résultat de chaque calcul qu'elle effectue. Cette instruction ne touche pas à l'exposant, mais seulement à la mantisse. La valeur par défaut de ces deux bits est 11 : notre FPU utilise donc des flottants double précision étendue. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754.
* 00 : mantisse codée sur 24 bits ;
* 01 : valeur inutilisée ;
* 10 : mantisse codée sur 53 bits ;
* 11 : mantisse codée sur 64 bits
|}
Les instructions x87 sont codées sur au minimum deux octets. Le premier octet commence toujours la suite de bit 11011, qui indique que c'est une instruction destinée au coprocesseur. Le 11011 était appelé le code d'échappement, sa mnémonique en assembleur était ESC. Le tout est suivi par 6 bits d'opcode, et 5 bits pour le mode d'adressage. Le tout était regroupé comme suit : 1101 1xxx aaxx xaaa, avec x les bits de l'opcode et a pour le mode d'adressage. Le tout était suivi par des opérandes, selon le mode d'adressage.
==Les coprocesseurs x87 et leurs précurseurs==
Les premiers coprocesseurs de ce type étaient l'Intel 8231/8232, destinés à être utilisés avec le 8088, un processeur 8 bits. Par la suite, Intel a récidivé avec le 8087, qui était destiné pour servir en tandem avec le 8086. Il a été suivi par les Intel 187, le 287, le 387, le 487 et le 587, qui étaient censés servir avec les CPU 186, 286, 386, 486, etc. Mais d'autres compagnies ont crée des coprocesseurs x87, comme Weitek, Cyrix, AMD, Texas Instrument, et bien d'autres.
===Les précurseurs : l'Intel 8231 et le 8232===
L'Am9511 et Am9512 sont une des toutes premières FPU pour PC, si ce n'est les premières si on en croit AMD. Ils ont été licenciés par Intel sous le nom d'Intel 8231 et le 8232. Ils étaient conçus pour complémenter le CPU Intel 8080, mais on pouvait parfaitement les utiliser avec d'autres CPU, comme le Z80. La [http://ep.homeserver.hu/PDF/AM9511A-9512.pdf documentation AMD donnait même des exemples assez variés]. La raison est qu'on y accédait comme n'importe quelle entrée-sortie connectée au bus système. Ils étaient accessibles via ''pooling'', interruptions ou même via DMA. Nous expliquerons comment c'est possible plus bas.
L'Intel 8231 ne supportait pas le jeu d'extension x87, qui est apparu après. Il gérait des nombres flottants de 32 bits, mais aussi des nombres en virgule fixe de 16 et 32 bits. Les flottants 32 bits suivaient globalement la norme IEEE 754, mais les nombres en virgule fixe utilisaient un format propriétaire. Il gérait les quatre opérations de base, mais aussi des calculs trigonométriques. Il utilisait pour cela du microcode, avec une approximation basée sur des polynômes de Tchebychev. L'intel 8232 supportait lui des flottants 32 et 64 bits, mais ne supportait que les quatre opérations de base (addition, soustraction, multiplication et division).
: La documentation décrit ces flottants 32 bits comme étant de la double précision, mais c'est parce que la terminologie de l'époque n'était pas encore bien stabilisée.
[[File:C8231A FPU PIN CONFIGURATION.png|thumb|Intel 8231 FPU PIN CONFIGURATION.]]
Les broches de l'intel 8231 sont illustrées ci-contre. La plupart des broches nous sont familières : 8 broches pour le bus de données (qui fait 8 bits), une entrée d'horloge, une entrée de RESET, une entrée ''chip select'' pour le décodage d'adresse. Les broches restantes sont très intéressantes, mais on les verra dans ce qui suit.
Toujours est-il que le coprocesseur est relié à un bus de 8 bits, alors que ses registres font 32 à 64 bits. Pour cela, le 8231/8232 lisait les opérandes octet par octet depuis le bus de données. Idem mais pour les écritures. Par contre, les instructions sont prévues pour faire 8 bits, pas plus. Pour avoir des instructions aussi courtes, la seule solution est d'utiliser une machine à pile et c'est ce que le 8231/8232 a fait. Précisons cependant que ce n'est pas la même pile de registre que la pile x87, mais c'était une sorte de pile similaire, qui a évolué pour donner la pile x87. Il s'agissait pour le coup d'une vraie pile, les opérations utilisaient systématiquement le sommet de la pile et l'opérande en dessous. Il n'y avait pas de possibilité d'adresser un opérande dans la pile.
Le processeur intégrait 8 registres de 16 bits, organisés comme une pile. Les registres pouvaient être utilisés : soit comme une pile de 8 opérandes 16 bits, soit une pile de 4 opérandes 32 bits, soit une pile de 2 opérandes 64 bits. Les opérandes étaient empilées octet par octet dans le processeur. Ils étaient dépilés là aussi octet par octet. Pour cela, le 8231/8232 dispose de trois entrées nommées A0, RD et WR. Les trois bits décident s'il faut faire une lecture, une écriture, exécuter une instruction, ou lire le registre d'état. Les quatre opérations sont appelées des commandes dans la documentation Intel et AMD. Les trois entrées font donc office de bus de commande simplifié.
{|class="wikitable"
|-
! A0, RD, WR !! Action
|-
| 000 || Lecture de l'octet depuis les registres de données.
|-
| 010 || Écriture de l'octet dans les registres de données.
|-
| 111 || L'octet est l'opcode d'une instruction, qui est exécutée immédiatement.
|-
| 101 || Lecture du registre d'état.
|}
Le processeur principal envoyait des commandes à l'Intel 8231/8232, qui les exécutait dans son coin. Le 8231/8232 envoyait un signal END OF EXECUTION pour prévenir qu'il avait fini son travail, que la commande précédente était terminée. Il avait une broche dédiée, appelée END, dédiée à ça. Le coprocesseur avait donc une interface de communication asynchrone, qui se voit quand on étudie ses broches. Les broches suivantes servent à la communication asynchrone avec le 8231/8232.
* READY est à 1 quand le 8231/8232 est libre, capable d'accepter une nouvelle instruction/commande. Il passe à 0 quand une instruction démarre, avec la commande 111 vue plus haut.
* END indique que la commande précédente a terminé son exécution. Lorsque END passe à 1, BUSY passe automatiquement à 0.
* EACK est une entrée sur laquelle le processeur dit qu'il a bien reçu le signal END, et que ce dernier peut être remis à 0.
Ce système pouvait être utilisé avec du ''pooling'', avec des interruptions, voire du DMA. Avec des interruptions, la sortie END était utilisée comme sortie d'interruption, reliée au CPU ou au contrôleur d'interruption. Pour le ''pooling'', le registre d'état du 8231/8232 contenait un bit BUSY, qui indiquait si le coprocesseur était utilisé ou non.
Un tel fonctionnement peut sembler étrange, et vous aurez l'impression que communiquer avec le coprocesseur est très lent. Mais cela prend tout son sens quand on connait le temps mis pour exécuter une instruction sur le coprocesseur. Une opération simple sur des flottants 32 bits prenait facilement une cinquantaine de cycles d'horloge, et c'était parmi les meilleurs temps de calcul. Il n'était pas rare d'avoir des opérations prenant plusieurs centaines, voire milliers de cycles d'horloge. Pas loin de 5000 cycles d'horloge pour une division de deux flottants 64 bits sur le 8232, plusieurs dizaines de milliers de cycles pour certaines opérations trigonométriques. Et le pire, c'était que c'était plus rapide que l'émulation logicielle !
Pas étonnant donc que le 8231/8232 aient été traités comme des entrées-sorties, à une époque ou tout était connecté sur un bus système assez rapide. Un autre avantage est que le 8231/8232 pouvaient fonctionner à une fréquence sans rapport avec celle du processeur. Par exemple, on pouvait utiliser un processeur à 1 MHz alors que le 8231/8232 allait à 4 MHz. Le coprocesseur faisait juste des calculs rapidement, comparé au CPU. Et ça a été utilisé sur certains systèmes Apple II. Ou encore, on pouvait utiliser un processeur légèrement plus rapide que le coprocesseur, avec quelques MHz de différence, comme un CPU à 5 MHz avec un coprocesseur de 2 MHz.
===L'intel 8087 et ses successeurs===
[[File:Intel 8087.svg|vignette|Intel 8087]]
Le 8087 été fabriqué avec 65 000 transistors. Le 8087 avait pour particularité qu'il était connecté directement sur le bus mémoire, au même titre que le 8086. Mais le 8087 n'avait pas de bus d'adresse et de données séparé. Le processeur utilisait un bus multiplexé. Il avait 20 broches pour se connecter au bus : 16 d'entre elles servaient alternativement de bus d'adresse et de données. L'interface avec le bus était donc un peu compliquée.
L'intel 387 était le coprocesseur associé au 386 d'Intel. Il était le premier coprocesseur à s'intégrer sur un bus de 32 bits. Il a été décliné en plusieurs versions, dont certaines sont spécifiques à un modèle de 386. Par exemple, le i386SX était une version simplifiée du 386 initial, qui avait notamment un bus de seulement 16 bits. Et de ce fait, il avait son propre coprocesseur i387SX, qui était adapté à un bus de 16 bits. De même, le i386SL était adapté aux ordinateurs portables et avait son propre coprocesseur i387SL.
Tout ce qui va suivre est valide pour tous les coprocesseurs x87 de marque Intel.
Le processeur central lisait des instructions, en envoyant le ''program counter'' sur le bus d'adresse, les instructions étaient récupérées sur le bus de données. Là, les deux processeurs déterminaient si l'instruction chargée était destinée au coprocesseur ou au CPU. Pour cela, les instructions x87 commencent toutes par la suite de bit 11011, qui permet de savoir facilement si une instruction est destinée au coprocesseur. Le 11011 était suivi par un opcode et un mode d'adressage.
Si le mode d'adressage demandait de lire un opérande mémoire, le 8086 envoyait l'adresse de l'opérande sur le bus, et le coprocesseur récupérait celle-ci sur le bus de données. Si l'opérande devait être lu en plusieurs fois, le coprocesseur lisait le reste de lui-même, en prenant le contrôle du bus d'adresse. Il récupérait l'adresse envoyée initialement par le CPU, puis l'incrémentait et relançait un nouvel accès mémoire. Il l'incrémentait autant de fois que nécessaire pour charger l'opérande.
Un problème est que le CPU ne sait pas combien de temps dure une instruction x87. Et cette durée dépendait de l'implémentation du processeur, elle n'était pas la même selon la marque du coprocesseur. Un 186 n'avait pas les mêmes timings que le 286, par exemple. Pour le CPU, une instruction x87 met juste deux cycles pour s'exécuter (plus si des opérandes doivent être lus en mémoire). Pour cela, le CPU disposait d'un mécanisme de synchronisation.
Le mécanisme de synchronisation était une instruction WAIT, qui forçait le CPU à attendre que le coprocesseur ait terminé l'instruction précédente. L'implémentation matérielle était assez simple. Le coprocesseur disposait d'une sortie BUSY, qui indiquait qu'il était en train d'exécuter une instruction et ne pouvait pas en accepter une nouvelle. Le CPU, quant à lui, avait une entrée TEST qui vérifiait si le, coprocesseur était occupé ou non. La sortie BUSY était reliée à l'entrée TEST. L'instruction test vérifiait juste ce qu'il y avait sur l'entrée TEST. Tant qu'elle était à 1, le processeur attendait et ne chargeait pas de nouvelle instruction. Dès qu'elle passe à 0, l'exécution reprend.
Il faut noter que l'instruction WAIT n'est nécessaire qu'entre deux instructions flottantes assez proches. Mais il est possible d'intercaler des instructions entières entre deux instructions flottantes. Le programme pouvait ainsi mixer instructions entières et flottantes, les instructions entières étant exécutées sur le 8086, les instructions flottantes sur le coprocesseur. Il y avait donc une possibilité de parallélisme, à savoir que les deux processeurs pouvaient exécuter des instructions différentes en même temps. Mais cela demandait que les calculs soient coopératifs et mélangent bien entiers et flottants.
Le 8087 et ses successeurs avaient une microarchitecture assez simple. L'unité de contrôle contenait un décodeur d'instruction microcodé, le registre de contrôle, le registre d'état. La plupart des instructions sont microcodées, l'unité de calcul est assez limitée. Elle permet d'additionner deux mantisses flottantes, de faire des décalages, d'additionner deux exposants, mais pas plus. Les multiplications et divisions sont donc microcodées et émulées en enchainant des additions flottantes. Les instructions trigonométriques sont implémentées en utilisant l'algorithme CORDIC, qu'on a vu dans le chapitre sur les circuits de calcul flottant.
[[File:Intel 8087 arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel 8087.]]
Le chemin de données est composé d'un banc de registre flottant pour la pseudo-pile, et de plusieurs circuits de calcul. Le banc de registre était mono-port, ce qui fait que les ALUs étaient précédés par deux registres temporaires pour les opérandes. Les circuits pour l'exposant et la mantisse sont séparés, et sont même reliés au banc de registre par deux bus séparés. Il y a un additionneur pour les exposants, un additionneur pour les mantisses et un décaleur pour les mantisses (pour les normaliser).
De plus, on trouve une mémoire ROM dédiée aux constantes les plus utilisées. Elle sert pour les constantes de base, gérées par le jeu d'extension x87. Mais elle contient aussi des constantes utilisées pour l'algorithme CORDIC. Elle n'est pas illustrée sur le schéma ci-dessous, mais elle existe.
L'interface avec le bus est un simple registre d’interfaçage avec le bus. Pour rappel, le bus de données fait 16 bits sur le 8087, 32 bits sur le 387. Entre le bus et le chemin de données, on trouve une file servant à simplifier la gestion des lectures. L'idée est que les opérandes lus/écrits font 32, 64 ou 80 bits, alors que le bus de données n'en fait que 16/32. Les opérandes sont donc lus/écrits en plusieurs passes. Sur le 8087, il doit réaliser deux passes pour des opérandes de 32 bits, quatre passes pour celles de 64 bits, 5 pour des opérandes de 80 bits (80 = 5 × 16). Le 387 doit faire deux fois moins.
[[File:Intel 387 arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel 387. Les circuits pour les exposants sont à gauche dans le chemin de données, les circuits pour les mantisses sont à droite.]]
L'implémentation du banc de registre est assez simple : une RAM avec un registre qui indique la position du sommet de la pile dedans. Le registre fait 3 bits, pour 8 registres. En plus de cela, il y a un petit soustracteur et un multiplexeur, pour adresser les opérandes dans la pile. Pour rappel, il est possible d'adresser la seconde opérande dans la pile. Mais on précise pas le numéro du registre dans la pile pour cela, on précise sa position sous le sommet de la pile, à savoir si elle est deux, trois, quatre opérandes sous le sommet de la pile. Pour déterminer quel registre lire, il faut soustraire ce "décalage" au numéro de registre du sommet de la pile. Pour cela, il y a un petit soustracteur pour faire le calcul.
Le circuit décaleur est composé de deux sous-décaleurs. Le premier fait des décalages au niveau des octets, le second décale l'opérande de 0 à 7 rangs.
Pour finir, voici quelques liens sur la microarchitecture du 8087 :
* [https://www.righto.com/2026/02/8087-instruction-decoding.html Instruction decoding in the Intel 8087 floating-point chip].
* [https://www.righto.com/2020/05/die-analysis-of-8087-math-coprocessors.html Die analysis of the 8087 math coprocessor's fast bit shifter].
* [https://www.righto.com/2025/12/8087-stack-circuitry.html The stack circuitry of the Intel 8087 floating point chip, reverse-engineered].
* [https://www.righto.com/2025/12/8087-microcode-conditions.html Conditions in the Intel 8087 floating-point chip's microcode].
* [https://www.righto.com/2020/05/extracting-rom-constants-from-8087-math.html Extracting ROM constants from the 8087 math coprocessor's die].
* [https://www.righto.com/2018/09/two-bits-per-transistor-high-density.html Two bits per transistor: high-density ROM in Intel's 8087 floating point chip].
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les sections critiques et le modèle mémoire
| prevText=Les sections critiques et le modèle mémoire
| next=L'accélération matérielle de la virtualisation
| nextText=L'accélération matérielle de la virtualisation
}}
</noinclude>
du2sjkg9utopzu42xiuiu2czb1dlmbc
Guide des mots rares à adopter/emphygomphe
0
83779
763025
762933
2026-04-05T21:41:23Z
ROSEMARSH HOOD
122846
763025
wikitext
text/x-wiki
== emphygomphe ==
''Nom féminin''
'''Définition :'''
'''Proclamation lyrique excessivement emphatique''', marquée par une exagération du ton et une importance démesurée accordée à des propos souvent creux. Par extension, ''emphygomphe'' désigne '''tout discours pompeux où l’effet oratoire l’emporte sur le contenu réel'''<ref>https://limiel.omeka.net/items/show/188</ref>.
''Exemple'' : Son discours d’intronisation ne fut qu’une emphygomphe tonitruante, où l’orgueil faisait plus de bruit que les idées.
=== '''Champ sémantique et usage''' ===
Le terme ''emphygomphe'' s’inscrit dans le champ du langage oratoire et de la critique stylistique. Il est particulièrement adapté pour qualifier des discours officiels, politiques ou cérémoniels qui abusent de grandiloquence et de formules spectaculaires, là où le français courant recourt à des expressions comme discours pompeux ou tirade emphatique.
Un discours peut être qualifié d’emphygomphe lorsqu’il '''privilégie l’apparat verbal, l’enflure rhétorique et l’autoglorification au détriment de la clarté,''' de la sincérité ou de la profondeur des idées.
=== '''Intérêt linguistique''' ===
''Emphygomphe'' enrichit la langue française en offrant un terme précis et expressif pour désigner une forme particulière d’excès oratoire. Sa sonorité dense et légèrement burlesque évoque elle-même l’enflure qu’elle décrit, ce qui en fait '''un mot particulièrement efficace dans les contextes critiques ou satiriques.'''
Dans une perspective littéraire ou stylistique, ce terme permet de nommer avec concision un défaut fréquent du discours, tout en apportant une nuance ironique héritée de la tradition rabelaisienne.
=== '''Étymologie''' ===
Formé par télescopage lexical après francisation, à partir du latin ''gomphus'' et du grec ''γόμφος'', combinés à l’affixe latin ''emphy-'', pour produire un terme à forte expressivité phonétique et stylistique.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/188 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 9 février 2026]
<small>(Lexème conçu pour la collection lexicale thématique « [https://limiel.omeka.net/collections/show/6 Dix néologismes conçus en hommage à François Rabelais] »)</small>
[[Catégorie:Français]]
[[Catégorie:Linguistique]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
rt4ui6pgtevbh0wumocmubpdb4nuv60
Guide des mots rares à adopter/brèviloquent, brèviloquente
0
83780
763028
762934
2026-04-05T21:42:25Z
ROSEMARSH HOOD
122846
763028
wikitext
text/x-wiki
== brèviloquent, brèviloquente ==
''Adjectif''
'''Définition :'''
Se dit de ce qui se caractérise par une expression sobre, concise et mesurée, '''privilégiant la brièveté dans la parole ou l’écriture'''. Par extension, ''brèviloquent'' qualifie un style, un discours ou une personne '''qui s’exprime avec économie de mots tout en conservant clarté et précision'''<ref>https://limiel.omeka.net/items/show/202</ref>.
''Exemple'' : Son discours brèviloquent captivait l’auditoire par son économie de mots et sa précision incisive.
=== '''Champ sémantique et usage''' ===
Le terme ''brèviloquent'' s’inscrit dans le champ de l’expression verbale et du style rédactionnel. Il est particulièrement '''utile pour décrire des formes de communication efficaces, épurées et rigoureuses,''' là où le français courant recourt à des expressions comme concis, bref ou peu loquace.
Un discours ou un style est dit brèviloquent lorsqu’il parvient à transmettre l’essentiel avec justesse, sans digression inutile ni surcharge verbale.
=== '''Intérêt linguistique''' ===
''Brèviloquent'' enrichit la langue française en proposant un adjectif précis pour désigner la concision maîtrisée dans l’expression. Issu du latin ''breviloquens'' (« qui parle brièvement »), il '''permet de distinguer la simple brièveté d’une véritable qualité stylistique fondée sur la densité et la pertinence du propos'''.
Dans les domaines littéraire, académique ou professionnel, ce terme valorise une forme d’élégance discrète fondée sur la retenue et l’efficacité du langage.
=== '''Étymologie''' ===
Du latin ''breviloquens'' (« qui parle en peu de mots »), francisé pour former un adjectif descriptif et stylistique.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/202 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 19 février 2026]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
jrs5oy99t7m7p8yiq1l9q07zhgb7tcs
Guide des mots rares à adopter/clausible
0
83781
763026
762938
2026-04-05T21:41:44Z
ROSEMARSH HOOD
122846
763026
wikitext
text/x-wiki
== clausible ==
''Adjectif'' <small>(voir aussi ''[[Guide des mots rares à adopter/clausibilité|clausibilité]]'')</small>
'''Définition :'''
'''Se dit de ce qui peut se refermer ou se sceller sur soi-même sans perte d’intégrité''', en conservant parfaitement son contenu ou sa structure. Par extension, ''clausible'' qualifie tout objet, système ou ensemble conçu pour être hermétiquement refermé de manière fiable<ref>https://limiel.omeka.net/items/show/290</ref>.
''Exemple'' : Ce contenant clausible se révèle particulièrement efficace pour préserver son contenu.
=== '''Champ sémantique et usage''' ===
Le terme ''clausible'' s’inscrit dans le champ des propriétés physiques et fonctionnelles des objets. Il est particulièrement '''utile pour décrire des contenants, dispositifs ou structures capables de se fermer de manière étanche et sécurisée''', là où le français courant recourt à des expressions comme refermable, hermétique ou à fermeture fiable.
Un objet est dit clausible lorsqu’il peut être fermé sans altération de son contenu, ni perte de substance, garantissant ainsi sa conservation ou son isolement.
=== '''Intérêt linguistique''' ===
''Clausible'' enrichit la langue française en proposant un adjectif précis pour désigner une propriété fonctionnelle spécifique, distincte de la simple possibilité de fermeture. Il introduit une nuance d’intégrité et de fiabilité dans l’acte de se refermer.
Dans les domaines technique, scientifique ou descriptif, '''ce terme permet d’exprimer avec concision une qualité''' souvent décrite par des périphrases plus longues, tout en conservant une élégance formelle.
=== '''Étymologie''' ===
Du latin ''clausibilis'' (« qui peut être fermé »), francisé pour former un adjectif descriptif et fonctionnel.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/290 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 28 mars 2026]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
kudr7612rs23yr7nbg3ql8e1vq4eusk
Guide des mots rares à adopter/clausibilité
0
83782
763027
762939
2026-04-05T21:42:01Z
ROSEMARSH HOOD
122846
763027
wikitext
text/x-wiki
== clausibilité ==
''Nom féminin'' <small>(voir aussi ''[[Guide des mots rares à adopter/clausible|clausible]]'')</small>
'''Définition :'''
Capacité d’un objet, d’un système ou d’un ensemble '''à se refermer sur lui-même ou à se sceller sans perte d’intégrité'''. Par extension, ''clausibilité'' désigne la qualité intrinsèque de ce qui peut être fermé de manière fiable tout en préservant son contenu<ref>https://limiel.omeka.net/items/show/296</ref>.
''Exemple'' : La clausibilité du contenu lexicographique m’apparaît époustouflante.
=== '''Champ sémantique et usage''' ===
Le terme ''clausibilité'' s’inscrit dans le champ des propriétés structurelles et fonctionnelles. Il est particulièrement '''adapté pour désigner, de manière abstraite, l’aptitude d’un dispositif ou d’un ensemble à être refermé efficacement''', là où le français courant recourt à des expressions comme capacité de fermeture ou aptitude à être hermétique.
Une structure possède une clausibilité lorsqu’elle garantit une fermeture complète, sans altération ni fuite de ce qu’elle contient.
=== '''Intérêt linguistique''' ===
''Clausibilité'' enrichit le lexique en introduisant un nom abstrait dérivé directement de ''clausible'', permettant de conceptualiser une propriété technique ou théorique avec précision. Il offre ainsi un pendant nominal utile dans les discours scientifiques, techniques ou analytiques.
Ce terme '''permet de condenser en un seul mot une notion complexe''', évitant des formulations longues et favorisant une expression plus rigoureuse et structurée.
=== '''Étymologie''' ===
Du latin ''clausibilis'' (« qui peut être fermé »), à l’origine de l’adjectif ''[[Guide des mots rares à adopter/clausible|clausible]]'', dont dérive le nom ''clausibilité''.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/296 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 29 mars 2026]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
gh94oz4f3y7pq1ffevie9kwvkaoz9sh
Catégorie:Guide des mots rares à adopter
14
83783
763014
2026-04-05T21:26:19Z
ROSEMARSH HOOD
122846
Ajout description
763014
wikitext
text/x-wiki
Cette catégorie rassemble toutes les pages et sous-pages du [[Guide des mots rares à adopter]].
r7xlakwj60r11valqrcak97e55lsyng
Guide des mots rares à adopter/culicellique
0
83784
763032
2026-04-05T23:25:06Z
ROSEMARSH HOOD
122846
ajout
763032
wikitext
text/x-wiki
== culicellique ==
''Adjectif''
'''Définition :'''
Se dit de ce qui rappelle par sa [[wikt:finesse|finesse]], sa [[wikt:légèreté|légèreté]] ou sa [[wikt:délicatesse|délicatesse]] '''l’aspect ou le [[wikt:comportement|comportement]] d’un tout petit insecte volant'''. Par extension, ''culicellique'' qualifie ce qui papillonne, frémit ou se meut avec agilité et subtilité<ref>https://limiel.omeka.net/items/show/201</ref>.
''Exemple'' : L’air culicellique de la clairière au matin frissonnait de chaque vibration légère, comme l’aile d’un petit insecte.
=== '''Champ sémantique et usage''' ===
Le terme ''culicellique'' s’inscrit dans le champ de la description sensorielle et poétique. Il est particulièrement '''utile pour évoquer des mouvements légers, des atmosphères aériennes ou des objets délicats''', là où le français courant recourt à des périphrases comme léger comme un insecte ou frémissant doucement.
Une personne, un objet ou un lieu peut être qualifié de culicellique lorsqu’il manifeste une grâce fragile, un mouvement subtil ou une légèreté aérienne.
=== '''Intérêt linguistique''' ===
''Culicellique'' enrichit le lexique français en offrant un adjectif '''précis pour exprimer légèreté, subtilité et finesse''', qualités difficiles à condenser en un seul mot. Sa formation, dérivée du latin ''culicellus'' (« petit moustique »), confère au terme une sonorité délicate et évocatrice, parfaitement adaptée à la poésie, à la littérature descriptive ou aux textes sensoriels.
Ce mot permet de transmettre en un seul terme la nuance fragile et aérienne d’un mouvement, d’une atmosphère ou d’un détail naturel.
=== '''Étymologie''' ===
Du latin ''culicellus'', diminutif de ''culex'' (« moustique »), francisé pour former un adjectif descriptif et poétique.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/201 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 19 février 2026]
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
snhsz52rlu8tvix3yvcwvj92lkeddci
763033
763032
2026-04-05T23:28:48Z
ROSEMARSH HOOD
122846
763033
wikitext
text/x-wiki
== culicellique ==
''Adjectif''
'''Définition :'''
Se dit de ce qui rappelle par sa [[wikt:finesse|finesse]], sa [[wikt:légèreté|légèreté]] ou sa [[wikt:délicatesse|délicatesse]] '''l’aspect ou le [[wikt:comportement|comportement]] d’un tout petit insecte volant'''. Par extension, ''culicellique'' qualifie ce qui papillonne, frémit ou se meut avec agilité et subtilité<ref>https://limiel.omeka.net/items/show/201</ref>.
''Exemple'' : L’air culicellique de la clairière au matin frissonnait de chaque vibration légère, comme l’aile d’un petit insecte.
=== '''Champ sémantique et usage''' ===
Le terme ''culicellique'' s’inscrit dans le champ de la description sensorielle et poétique. Il est particulièrement '''utile pour évoquer des mouvements légers, des atmosphères aériennes ou des objets délicats''', là où le français courant recourt à des périphrases comme léger comme un insecte ou frémissant doucement.
Une personne, un objet ou un lieu peut être qualifié de culicellique lorsqu’il manifeste une grâce fragile, un mouvement subtil ou une légèreté aérienne.
=== '''Intérêt linguistique''' ===
''Culicellique'' enrichit le lexique français en offrant un adjectif '''précis pour exprimer légèreté, subtilité et finesse''', qualités difficiles à condenser en un seul mot. Sa formation, dérivée du latin ''culicellus'' (« petit moustique »), confère au terme une sonorité délicate et évocatrice, parfaitement adaptée à la poésie, à la littérature descriptive ou aux textes sensoriels.
Ce mot permet de transmettre en un seul terme la nuance fragile et aérienne d’un mouvement, d’une atmosphère ou d’un détail naturel.
=== '''Étymologie''' ===
Du latin ''culicellus'', diminutif de ''culex'' (« moustique »), francisé pour former un adjectif descriptif et poétique.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/201 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 19 février 2026]
[[Catégorie:Linguistique]]
[[Catégorie:Français]]
[[Catégorie:Guide des mots rares à adopter]]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
cokk5wr7v76x65xdcmhtfigddsdv6a3
Guide des mots rares à adopter/imbrigène
0
83785
763034
2026-04-05T23:35:25Z
ROSEMARSH HOOD
122846
ajout de
763034
wikitext
text/x-wiki
== imbrigène ==
''Adjectif''
'''Définition :'''
Se dit de ce qui est '''[[wikt:naître|né]] de la [[wikt:pluie|pluie]] ou a été [[wikt:façonner|façonné]] par les [[wikt:goutte d’eau|gouttes d’eau]]'''. Par extension, ''imbrigène'' qualifie tout lieu, élément naturel ou phénomène directement influencé par la pluie ou l’humidité<ref>https://limiel.omeka.net/items/show/200</ref>.
''Exemple'' : Les marais imbrigènes couvrent une vaste partie de notre territoire ancestral.
=== '''Champ sémantique et usage''' ===
Le terme ''imbrigène'' s’inscrit dans le champ de la description géographique et écologique. Il est particulièrement '''utile pour évoquer des zones humides, des paysages pluvieux ou des éléments naturels créés par l’action de l’eau''', là où le français courant recourt à des périphrases comme né de la pluie ou façonné par les eaux.
Un terrain, un marais ou une végétation peut être qualifié d’imbrigène lorsqu’il témoigne directement de l’influence et de la présence persistante de la pluie.
=== '''Intérêt linguistique''' ===
''Imbrigène'' enrichit le lexique français en offrant un adjectif poétique et précis pour '''désigner des effets ou des origines liés à la pluie'''. Sa formation, dérivée du latin ''imbrigenus'' (« né de la pluie »), combine exactitude descriptive et dimension imagée, permettant d’évoquer avec concision des paysages humides et fertiles.
Ce mot est particulièrement adapté à la littérature naturaliste, à la poésie ou à toute description où l’eau et la pluie sont des acteurs essentiels du décor.
=== '''Étymologie''' ===
Du latin ''imbrigenus'' (« né de la pluie »), francisé pour former un adjectif descriptif et poétique.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/200 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 18 février 2026]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Français]]
[[Catégorie:Linguistique]]
[[Catégorie:Guide des mots rares à adopter]]
qmmcqcoa495d5t9hjqqptysmmav0h8p
Guide des mots rares à adopter/fâtiloque
0
83786
763035
2026-04-05T23:42:11Z
ROSEMARSH HOOD
122846
ajout au guide
763035
wikitext
text/x-wiki
== fâtiloque ==
''Adjectif''
'''Définition :'''
Se dit de ce qui '''[[wikt:prédire|prédit]] l’[[wikt:avenir|avenir]] ou [[wikt:connaître|connaît]] le [[wikt:destin|destin]]'''. Par extension, ''fâtiloque'' qualifie toute personne ou entité qui détient une voix sacrée, capable d’annoncer ou de révéler ce qui est à venir<ref>https://limiel.omeka.net/items/show/199</ref>.
''Exemple'' : J’ai toujours su que cette dame était une voyante fâtiloque.
=== '''Champ sémantique et usage''' ===
Le terme ''fâtiloque'' s’inscrit dans le champ de la divination, du présage et du mysticisme. Il est particulièrement adapté pour '''désigner des individus, des oracles ou des écrits réputés pour leur capacité à révéler l’avenir''', là où le français courant recourt à des expressions comme voyant, prophétique ou devin.
Une personne ou un discours peut être qualifié de fâtiloque lorsqu’il inspire confiance par sa sagesse, son autorité ou son pouvoir de révélation sur le destin.
=== '''Intérêt linguistique''' ===
''Fâtiloque'' enrichit le vocabulaire français en offrant un adjectif précis et poétique pour '''désigner la capacité de prédire ou de connaître le futur'''. Sa formation, dérivée du latin ''fatiloquus'' (« qui parle du destin »), confère au mot un caractère littéraire et solennel, idéal pour les contextes narratifs, poétiques ou historiques.
Ce mot permet d’exprimer en un seul terme la notion de voix prophétique ou sacrée, évitant des périphrases plus lourdes et renforçant l’effet stylistique du texte.
=== '''Étymologie''' ===
Du latin ''fatiloquus'' (« qui parle du destin »), francisé pour former un adjectif descriptif et littéraire.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/199 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 18 février 2026]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
[[Catégorie:Français]]
[[Catégorie:Linguistique]]
8vcnrhm9nsn6th8nn8c9r2ijexywt07
Guide des mots rares à adopter/fatilégue
0
83787
763036
2026-04-05T23:49:05Z
ROSEMARSH HOOD
122846
ajout au guide
763036
wikitext
text/x-wiki
== fatilégue ==
''Adjectif''
'''Définition :'''
Se dit de ce qui '''[[wikt:récolter|récolte]] les [[wikt:âme|âmes]] ou [[wikt:colectionner|collectionne]] les [[wikt:mort|morts]]'''. Par extension, ''fatilégue'' qualifie tout être, figure ou allégorie associé à la mort et à la transition des vivants vers l’au-delà<ref>https://limiel.omeka.net/items/show/198</ref>.
''Exemple'' : La grande faucheuse, allégorie fatilégue.
=== '''Champ sémantique et usage''' ===
Le terme ''fatilégue'' s’inscrit dans le champ des représentations funéraires et symboliques de la mort. Il est particulièrement adapté pour '''désigner des figures mythiques, des entités littéraires ou des concepts métaphysiques liés au passage de la vie à la mort''', là où le français courant utilise des expressions comme faucheuse, passeuse d’âmes ou collectionneuse de morts.
Une allégorie, un personnage ou une figure peut être qualifié de fatilégue lorsqu’il incarne la fonction de guide ou de moissonneur des âmes, avec gravité et solennité.
=== '''Intérêt linguistique''' ===
''Fatilégue'' enrichit la langue française en offrant un adjectif précis et évocateur pour '''désigner la mort personnifiée ou ses agents symboliques'''. Sa formation, dérivée du latin ''fatilegus'' (« qui récolte le destin ou les âmes »), confère au mot une sonorité dramatique et poétique, adaptée à la littérature, à la mythologie ou à l’écriture allégorique.
Ce terme permet de condenser en un seul mot une idée complexe et solennelle, renforçant ainsi l’impact stylistique et imaginaire du texte.
=== '''Étymologie''' ===
Du latin ''fatilegus'' (« qui récolte les âmes »), francisé pour former un adjectif descriptif et poétique.
=== '''Source''' ===
[https://limiel.omeka.net/items/show/198 Lexique informatisé des mots insolites à étymologie latine (LiMiEL), le 18 février 2026]
<references />
<small>< [[Guide des mots rares à adopter#Sommaire|Retour au sommaire]]</small>
[[Catégorie:Guide des mots rares à adopter]]
[[Catégorie:Français]]
[[Catégorie:Linguistique]]
b3i777det3esckq6pzc5awdpwf953z8