Aller au contenu

Fonctionnement d'un ordinateur/L'adressage des périphériques

Un livre de Wikilivres.

Dans le chapitre précédent, nous avons vu comment que contrôleur de périphérique dispose de registres d’interfaçage, dans lequel le processeur écrit des commandes ou lit l'état du périphérique. Les registres interfaçage disposent pour cela d'une adresse, similaire à l'adresse mémoire. Mais nous n'avons pas vu comment le processeur utilise ces adresses. 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.

Rappels : l'espace d'adressage unifié ou séparé

[modifier | modifier le wikicode]

Voyons d'abord comment les adresses des registres d’interfaçage sont gérées par le processeur. Pour faire simple, il y a deux solutions. 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.

L'espace d’adressage séparé

[modifier | modifier le wikicode]

La première solution sépare les adresses mémoire et les adresses périphériques, qui ne sont pas transmises sur les mêmes bus. 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.

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

[modifier | modifier le wikicode]

La seconde solution s'appelle l'espace d'adressage unifié, ou encore les entrées-sorties mappées en mémoire. Elle mélange adresse mémoire et adresses d'entrée-sorties, dans un seul espace d'adressage, un seul ensemble d'adresse. 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.

IO mappées en mémoire

L'avantage de cette méthode est qu'il n'y a pas besoin d'instructions différentes pour accéder aux périphériques et à la mémoire. Le processeur 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 les adresses associées aux périphériques. Les transferts entre mémoire et périphériques sont fortement simplifiés.

Le décodage d'adresse et son implémentation

[modifier | modifier le wikicode]

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 : les bus systèmes, les bus mémoire séparés des bus d'entrées-sorties séparés, et les systèmes à base de répartiteurs.

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.

Bus unique avec entrées mappées en mémoire.

La seconde utilise des bus séparés pour la mémoire et les entrées-sorties. Elle utilise un bus mémoire et un ou plusieurs bus d'entrée-sortie, aussi appelés bus I/O (I/O pour input Output, ce qui veut dire Entrée-sortie)

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.

IO mappées en mémoire avec séparation des bus, usage d'un répartiteur

Intuitivement, le bus système va de concert avec des entrées-sorties mappées en mémoire, alors qu'utiliser un bus séparé pour les entrées-sorties implique des espaces d'adressage séparés. Et dans les grandes lignes, c'est pas trop faux. Un bus système peut implémenter les deux solutions d'adressage, et c'est la même chose avec un répartiteur. 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.

Espace d'adressage unifié (entrées-sorties mappées en mémoire) Espace d'adressage séparé
Bus système 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, presque obligatoire

Les entrées-sorties mappées en mémoire avec un bus système

[modifier | modifier le wikicode]

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.

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.

Exemple détaillé.

Sur quelques consoles/ordinateurs, il est arrivé que le décodage d'adresse soit partiel, à savoir que quelques bits d'adresse étaient ignorés. Le circuit de décodage d'adresse était alors plus simple, moins cher. Le résultat était que des mémoires ou des entrée-sorties étaient dupliquées dans l'espace d'adressage. Un exemple parlant est celui de la console NES. Elle avait 2 kibioctet de RAM, qui apparaissait quatre fois dans l'espace d'adressage, dans les 8 premiers kibioctets (adresses basses). Idem avec les registres d’interfaçage de la carte graphique : il y en avait 1024 copies ! Les registres prenaient 8 octets en tout, mais il y en avait assez de copies pour remplir 8 kibioctets de l'espace d'adressage... C'était totalement inutile, c'était une conséquence d'économie de circuits.

Intervalles d'adresses Description Taille
$0000–$07FF Mémoire RAM 2 kibioctets
$0800–$0FFF Mémoire RAM (miroir) 2 kibioctets
$1000–$17FF Mémoire RAM (miroir) 2 kibioctets
$1800–$1FFF Mémoire RAM (miroir) 2 kibioctets
$2000 - $3FFF Registres d’interfaçage de la carte graphique, une copie tous les 8 octets 8 kibioctets
$4000–$401F Divers
$4020–$FFFF Inoccupé, utilisé par la cartouche de jeu

L'espace d'adressage séparé avec un bus système

[modifier | modifier le wikicode]

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.

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.

Espace d'adressage séparé.

Les entrées-sorties mappées en mémoire avec des configurations de bus spéciales

[modifier | modifier le wikicode]

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.

IO mappées en mémoire avec séparation des bus

Les bus d'entrée-sortie : multiplexage, adressage et coprocesseurs I/O

[modifier | modifier le wikicode]

Les bus I/O connectent un processeur à des entrées-sorties et à rien d'autre. Ils ont l'avantage d'être plus simples que le bus mémoire. Cependant, il y a rarement un bus d'entrées-sortie pour chaque entrée-sortie présente dans l'ordinateur. À la place, il y a un seul bus d'entrée-sortie qui est partagé entre plusieurs entrées-sorties, généralement entre plusieurs contrôleurs de périphériques. Pour gérer plusieurs contrôleurs de périphériques, il y a plusieurs solutions.

La première utilise des adresses réservées. Le processeur a un espace d'adressage réservé pour les entrées-sorties, dans lequel chaque registre d’interfaçage a sa propre adresse réservée. Les contrôleurs de périphériques surveillent le bus et analysent chaque transaction dessus, pour voir si une transaction leur est destinée. Pour cela, ils surveillent les adresses sur le bus d'adresse et réagissent quand une adresse envoyée est associée à un de leurs registres d’interfaçage. Le bus d'adresse est généralement plus petit que celui du bus mémoire.

Bus entre processeur et contrôleur de périphérique.

Mais d'autres solutions alternatives utilisent un mélange entre bus d'I/O dédié et bus à répartiteur.

Le multiplexeur d'entrée-sortie

[modifier | modifier le wikicode]

Les anciens ordinateurs des années 80-90 utilisaient une solution alternative, qui est un mélange entre bus d'I/O dédié et bus à répartiteur. 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'IO MUX 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.

IO MUX

Les ports IO étaient banalisés, à savoir qu'on pouvait brancher n'importe quoi dessus. Il était possible de brancher un capteur de température, un moteur à commander, un port série, un port parallèle, un écran, un clavier, une souris, peu importe. D'ordinaire, un port IO peut fonctionner soit en tant qu'entrée, soit en tant que sortie. Il est même possible de changer de sens en cours de fonctionnement, pour passer d'une entrée à une sortie ou inversement.

Les ports banalisés de ce type sont parfois appelés des GPIO, abréviation de General Purpose Input/Output. On ne les trouver pas que sur les IO MUX, le terme est utilisé pour tout port, regroupant plusieurs broches d'entrée-sortie, qui peuvent être utilisés à volonté. De telles broches sont généralement reliées directement ou indirectement au processeur, qui décide quoi envoyer sur des broches et comment interpréter ce qui est reçu dessus (le logicel sur le processeur, du moins).

L'Intel 8255 : un MUX IO ancien

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

Précisons que le 8255 était plus complexe que ce qui vient d'être 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é.

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 PIO des Rasberry Pi

[modifier | modifier le wikicode]

Les multiplexeurs d'entrée-sortie peuvent être améliorés en leur rajoutant des fonctionnalités de manipulation bit à bit, ainsi que des décalages. Les décalages sont très utiles pour communiquer avec certaines entrées-sorties aux bus très petits. Par exemple, imaginez un IO MUX qui gère 8 ports de 32 bits. Pour connecter dessus une entrée-sortie 16 bits, on doit transmettre les données en deux fois, 16 bits à la fois. Il est possible de configurer l'IO MUX de manière à ce qu'il applique un masque sur les données, par exemple pour en mettre certains bit à 0 ou à 1.

Les IO MUX les plus élaborés deviennent des coprocesseurs d'entrées-sorties, aussi appelés coprocesseurs I/O. J'avais dit dans le chapitre précédent que les coprocesseurs I/O étaient des contrôleurs DMA améliorés, mais c'était en réalité une simplification. Il existe un second type de coprocesseurs I/O, qui est dérivé des IO MUX. L'idée est de combiner un IO MUX et un mini-processeur dédié, au jeu d'instruction spécialisé dans les manipulations bit à bit. Le tout peut être complété par des mémoires FIFOs pour simplifier l’interfaçage avec le processeur et les entrées-sorties.

Raspberry Pi GPIO

L'exemple que nous allons voir est le PIO intégré dans les microcontrôleurs RP 2040 et consorts, utilisés dans les produits de la gamme Rasberry Pi et leurs dérivés. Il est connecté à un port extérieur, illustré ci-contre, qui regroupe 32 broches d'entrée et de 32 broches de sortie. Notez que je n'ai pas parlé de ports, pour une raison simple : le nombre de port est configurable ! Les 32 broches d'entrée et de sortie peuvent être regroupées dans 1 à 4 ports, qui ont chacun la taille qu'on veut. Par exemple, on peut regrouper les 32 broches de sortie en 4 ports de 8 bits, ou en 32 ports série (1 bit), ou en un port de 12 bits, un port de 16 bits et un port de 4 bits. La seule contrainte est que les broches d'un même port doivent être consécutives.

Le PIO regroupe un IO MUX avec gestion des interruptions, des mémoires FIFOs, mais aussi et surtout : des coprocesseurs IO ! Le PIO est fourni avec des programmes permettant de communiquer avec des UART, ou des bus SPI et I2C. Ils sont exécutés chacun sur un coprocesseur I/O. D'où le fait que certaines broches soit attribuées de base à ces bus. Rassurez-vous : elles peuvent être réattribuées à volonté. Le fait que les coprocesseurs soient programmables permet de gérer n'importe quel bus ou entrée-sortie.

Chaque coprocesseur I/O communique avec un port rien qu'à lui. La configuration d'un port, à savoir quelles broches il utilise, est le fait d'un registre de configuration pour chaque coprocesseur I/O. La configuration se faisant par coprocesseur, deux coprocesseurs peuvent partager une même broche ! Un coprocesseur, il peut adresser les broches de deux manières : soit en utilisant le numéro de broche général compris entre 0 et 31, soit en utilisant la position d'une broche dans son port dédié.

Pour se connecter à n'importe quelle entrée-sortie, le coprocesseur doit se cadencer à la même fréquence que celle de l'entrée-sortie. Pour cela, les registres de configuration permettent de configurer la fréquence du coprocesseur. Il est possible de multiplier la fréquence de base de ce coprocesseur par un coefficient, encodé sur 24 bits, en virgule fixe, avec une partie entière de 16 bits et une partie fractionnaire de 8 bits.

Les coprocesseurs I/O partagent une mémoire d'instruction, capable de mémoriser 32 instructions maximum. C'est une mémoire multiport, avec un port d'écriture et quatre ports de lecture. Le port d'écriture sert juste à charger les programmes à exécuter dedans. Les 4 ports de lecture permettent aux 4 coprocesseurs d'exécuter une instruction par cycle. Notons que les 4 coprocesseurs peuvent exécuter des programmes différents. C'est plus pratique que d'utiliser une mémoire d'instruction par coprocesseur.

Les 8 mémoires FIFO regroupent 4 FIFO d'entrée et 4 FIFOs de sortie, mais on peut les reconfigurer pour avoir 8 FIFOs allant dans le même sens.

PIO du Rasberry Pi - RP2040.

Les coprocesseurs IO sont très simples et exécutent des programmes très courts. Ils supportent 9 instructions, appelées JMP, WAIT, IN, OUT, PUSH, PULL, MOV, IRQ, et SET. Ils intègrent un program counter, un registre d'état et des registres de configuration, mais aussi des registres pour les données. Il y a deux registres X et Y qui servent à manipuler des données, ainsi qu'un registre NULL qui contient toujours zéro. Les deux autres registres sont les registres In Shift et Out Shift, dont le nom trahit deux choses : ce sont des registres à décalages, ils servent à échanger des données avec l'extérieur. Pour être précis, ils servent d'intermédiaire entre les broches/ports et les mémoires FIFO. Voyons comment.

Le registre Out Shift est altéré par deux instructions. L'instruction PULL lit un nombre de 32 bits dans la mémoire FIFO, et le copie dans le registre Out Shift. L'instruction OUT est assez complexe. Pour simplifier, elle transfère la donnée dans le registre Out Shift vers une broche, bit par bit. Pour cela, le registre Out Shift décalé à chaque cycle d'horloge, automatiquement. Il s'agit du principe de base, mais qui est cependant altéré par les fonctionnalités suivantes :

  • Il est possible de faire le transfert non pas bit par bit, mais par groupes de N bits, N étant compris entre 1 et 32 (inclus).
  • Le contenu du registre est envoyé non pas sur une broche, mais ailleurs, d'autres destinations étant possibles. Par exemple, les deux registres X et Y peuvent servir de source ou de destination.
  • Il est possible de configurer le coprocesseur pour faire N transferts de 32 bits à la suite, sans avoir à utiliser de boucles. Le nombre d'itérations est placé dans un autre registre et est décrémenté à chaque itération, automatiquement, sans utiliser d'instruction séparée.
  • On peut configurer le décalage pour qu'il se fasse de droite à gauche ou de gauche à droite.

Le registre In Shift fait l'inverse du registre Out Shift. Il est manipulé par deux instructions nommés IN et PUSH. L’instruction IN récupère un flux de bits sur une broche, l'accumule dans le registre In Shift, jusqu'à avoir récupéré une donnée de 32 bits complète. L'instruction PUSH copie le contenu du registre In Shift dans une mémoire FIFO. Le registre In Shift est mis à zéro après une instruction PUSH. Les possibilités lisétes plus haut sont aussi valides pour l'instruction IN.

Pour faire les décalages, le coprocesseur intègre deux registres qui mémorisent par combien il faut décaler. Il y en a un poiur le registre In Shift et un autre pour le registre Out Shift.

Nous venons de voir quatre instructions sur les 9 possibles : IN, OUT, PUSH, PULL. L'instruction JMP est un branchement conditionnel, rien de spécial de ce point de vue. Les instructions MOV, SET, WAIT et INT sont bien plus intéressantes.

L'instruction SET écrit une constante immédiate de 5 bits, dans une destination. La destination peut être le registre X ou Y, ou des broches de sortie. Quand on écrit la constante dans les registres X ou Y, la constante est écrite dans les 5 bits de poids faible, les autres bits sont mis à 0. Il n'est pas possible d'écrire dans les registres In Shift et Out Shift.

L'instruction MOV porte assez mal son nom, car elle ne fait pas que copier un registre dans un autre, c'est ici une instruction beaucoup plus complexe et performante. Elle peut copier une donnée d'une source vers une destination, mais aussi appliquer une opération bit à bit sur la donnée. L'opération en question est soit une opération NOT (inversion des bits), ou inverser l'ordre des bits (les bits de poids fort deviennent ceux de poids faible et inversement).

La source d'un MOV est n'importe quel registre de données : X, Y, In Shift, Out Shift, mais ausi le registre NULL (pour le zéro). Il est aussi possible de lire directement les 32 broches d'entrée d'un seul coup, ou encore de lire le registre d'état du coprocesseur. Pour les sorties, il est possible d'écrire dans les registres de données, sauf le registre NULL. Il est aussi possible d'écrire dans les 32 broches de sortie directement, sans passer par le registre Out Shift. Plus surprenant, il est possible d'écrire dans le Program Counter, afin de faire un branchement inconditionnel ! Et encore plus surprenant, il est possible d'écrire dans le registre d'instruction, ce qui permet d'exécuter une donnée comme une instruction !

Le coprocesseur dispose de 8 sorties d'interruption, numérotées de 0 à 7. Sur les 8, 4 sont reliées au contrôleur d’interruptions, 4 sont internes au coprocesseur. L'instruction INT peut lever une interruption, en mettant une sortie d'interruption à 1. L'instruction INT précise quelle sortie mettre à 1, elle encode trois bits pour cela. Elle peut aussi mettre à 0 cette sortie d'interruption. Il y a aussi une option pour décider si la sortie d'interruption est immédiatement remise à zéro, ou si le processeur doit la remettre à 0 lui-même (demande d'ACK).

L'instruction WAIT met en pause le processeur tant qu'une condition bien spécifique n'est pas remplie. La condition en question est l'envoi d'un 0 ou d'un 1 sur une broche bien précise. La broche en question soit une des 32 broches d'entrée, soit une entrée d'interruption. Pour le dire autrement, le processeur peut être réveillé par une interruption, ou par un signal bien précis sur une broche bien précise.

Pour résumer, les coprocesseurs I/O de ce type sont spécialisés dans la manipulation de bit, et ne gèrent que des décalages et des opérations de masquage. Pas d'opérations arithmétiques, un minimum de branchements, peu de registres. Par contre, ils ont des fonctionnalités de timing et de délais très poussées, comme on s’apprête à le voir en détail. Les instructions sont toutes encodées sur 16 bits et elles s'exécutent toutes en un seul cycle d'horloge. Leur encodage est décrit dans ce tableau, et vous devriez vous poser une question en le lisant : mais à quoi correspond ce champ "Délai/side set" ? Les explications vont suivre.

Jeu d'instruction des coprocesseurs du PIO
Instruction 3 bits (Opcode ) 5 bits 3 bits 5 bits
JMP 000 Délai/side set Condition Adresse de destination
WAIT 001 Délai/side set Source Numéro d'interruption ou de broche
IN 010 Délai/side set Source Bit count
OUT 011 Délai/side set Destination Bit count
PUSH 100 Délai/side set 0, deux bits de configuration 00000
PULL Délai/side set 1, deux bits de configuration 00000
MOV 101 Délai/side set Destination Opération (2 bits), Source (3 bits)
IRQ 110 Délai/side set 0, clear, wait Numéro d'interruption
SET 111 Délai/side set Destination Constante immédiate

Communiquer avec une entrée-sortie demande d'envoyer des données au bon moment, et de les recevoir au bon moment. Le timing est donc crucial. Pour cela, chaque instruction peut forcer le processeur à attendre N cycles d'horloge avant l'instruction suivante. Le nombre N est encodé dans l'instruction elle-même, sur quelques bits, entre 0 et 5. Cette possibilité est très utile pour timer les envois de données à une entrée-sortie, chose très importante. Nous l’appellerons la fonctionnalité de retard forcé.

Les instructions ont aussi une fonctionnalité de Side-set, qui permet d'écrire entre 0 et 5 bits sur certaines broches, sans utiliser d'instruction OUT/MOV/SET. Toutes les instructions peuvent utiliser un side set, y compris OUT, SET et MOV. La constante à écrire sur les broches est encodée dans l'instruction, c'est une constante immédiate. Les broches sont configurées au démarrage du programme, en écrivant le registre PINCTRL_SIDESET_BASE.

La constante de side set et le retard forcé se partagent le champ de 5 bits "Délai/side set". Les bits de poids fort sont réservés au side set, les bits de poids fort sont pour le délai forcé. Le nombre de bits attribués au side set dépend de la configuration du registre PINCTRL_SIDESET_COUNT. Les bits restants sont utilisés pour la fonction de retard forcé.

Les coprocesseurs disposent aussi d'une fonctionnalité de program wrapping. Il permet de répéter en boucle le même programme, sans avoir à utiliser de branchements inconditionnels. Quand on atteint la fin du programme, le processeur redémarre directement au début du programme, il passe directement de la dernière instruction à la première. Il faut pour cela configurer l'adresse de la première instruction et de la dernière instruction, et activer l'option dans le registre de configuration adéquat. Cela permet de simplifier la gestion des timings (un branchement prend un cycle d'horloge, et peut donc décaler temporellement des instructions), sans compter qu'on économise des instructions. Je rappelle pour ce dernier point que la mémoire d'instruction ne peut mémoriser que 32 instructions max, à partager entre plusieurs programmes...

L'implémentation du DMA suivant le bus utilisé

[modifier | modifier le wikicode]

Un bus système est l'idéal pour implémenter le DMA. Le contrôleur DMA est alors connecté au bus et il se réserve l'accès au bus quand il effectue un transfert DMA.

Controleur DMA

L'usage d'un répartiteur ne pose pas de problèmes particuliers pour 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.

Implémentation du DMA avec un répartiteur

L'usage d'un bus I/O ne permet pas l'implémentation du Direct Memory Access. Les communications entre I/O et RAM passeront forcément 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