Fonctionnement d'un ordinateur/Les méthodes de synchronisation entre processeur et périphériques
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
[modifier | modifier le wikicode]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.

Les registres d’interfaçage
[modifier | modifier le wikicode]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.

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
[modifier | modifier le wikicode]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.

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.

Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. À 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. À 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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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.

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.

Les entrées d'interruption du processeur
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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.
À 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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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.

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.

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 entrées à 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.

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
[modifier | modifier le wikicode]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.

Les contrôleurs d'interruption en cascade
[modifier | modifier le wikicode]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.


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
[modifier | modifier le wikicode]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.

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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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.

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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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.


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.

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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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.

DMA et cohérence des caches CPU
[modifier | modifier le wikicode]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.

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
[modifier | modifier le wikicode]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.

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
[modifier | modifier le wikicode]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.

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

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.

Le chipset de la carte mère
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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.

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.

Un exemple de chipset pour le bus ISA
[modifier | modifier le wikicode]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).

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.

Les coprocesseurs d'entrée-sorties
[modifier | modifier le wikicode]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.

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. À 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
[modifier | modifier le wikicode]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.

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.

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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]
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. À 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.