Les chapitres précédents ont couvert les puces mémoire, les puces de CPU et les bus qui les relient. La dernière pièce, c’est la façon dont le CPU atteint le monde extérieur : les circuits d’interface d’E/S, et le décodage d’adresses qui décide quelle puce répond quand le CPU place une adresse sur le bus. Un petit ordinateur embarqué en fait un exemple clair ; les mêmes principes font tourner un PC moderne, simplement cachés dans le chipset et dans la configuration de PCI Express.
Un circuit d’E/S, c’est quelques registres
Pour le CPU, un circuit d’E/S ressemble à une poignée de registres à des adresses fixes. La plupart en ont trois sortes :
- des registres de données, par lesquels les octets entrent et sortent ;
- des registres d’état, qui disent si le périphérique est prêt, occupé ou en erreur ;
- des registres de contrôle, qui règlent le mode du périphérique : vitesse, sens, interruptions activées ou non.
Tout le reste (pousser des bits sur un fil, échantillonner un capteur, attendre une imprimante lente) se passe à l’intérieur du circuit.
L’UART (universal asynchronous receiver-transmitter) en est l’exemple classique. Il prend un octet écrit dans son registre de données et l’envoie bit à bit sur une ligne série : un bit de start, 8 bits de données et un bit de stop, soit 10 bits par octet dans le format courant « 8N1 ». Les anciens terminaux et modems utilisaient des vitesses de 50 à 19 200 bit/s. L’UART 16550 du PC, avec son quartz standard de 1,8432 MHz, montait jusqu’à 1,8432 MHz / 16 = 115 200 bauds, soit 11 520 octets par seconde en 8N1 ; les puces USB-série qui l’ont remplacé montent à plusieurs mégabauds. Les UART sont tout sauf morts : chaque microcontrôleur en a un ou plusieurs, et c’est la première chose qu’un ingénieur branche pour démarrer une nouvelle carte. Linux nomme encore son pilote d’UART d’après la puce du premier IBM PC, et la VM Linux utilisée pour ces chapitres l’a :
$ ls /sys/bus/platform/devices
40000000.pci
Fixed MDIO bus.0
alarmtimer.0.auto
gpio-keys
hypervisor
psci
psci-cpuidle
serial8250
timer
vhci_hcd.0
serial8250 est le pilote de l’UART 8250 et de ses descendants, dont le 16550.
Le PIO (parallel I/O) est l’autre circuit d’interface classique, inspiré du 8255A d’Intel. Il a trois ports de 8 bits, A, B et C, dont chacune des 24 broches peut être lue ou pilotée, et un registre de contrôle qui dit quels ports sont des entrées et lesquels sont des sorties. Côté CPU, il a 8 broches de données, deux broches d’adresse A0–A1 qui choisissent l’un de ses quatre registres (ports A, B, C et contrôle), une sélection de puce, des signaux de lecture et d’écriture, et une remise à zéro. (Un modèle simplifié n’a besoin que de 3 bits de contrôle, un bit de sens par port. Le mot de contrôle du vrai 8255A a 8 bits, parce qu’il choisit aussi entre trois modes de fonctionnement et peut couper le port C en deux moitiés indépendantes.) Le premier IBM PC utilisait un 8255 aux adresses d’E/S 0x60 à 0x63 pour lire le clavier et les interrupteurs de configuration et piloter le haut-parleur, et aujourd’hui encore le contrôleur de clavier d’un PC répond au port 0x60.
Projetées en mémoire ou par ports
Comment le CPU adresse-t-il ces registres ? Il y a deux réponses.
Les E/S par ports (port-mapped I/O) donnent aux périphériques un espace d’adressage séparé, atteint par des instructions spéciales. Le x86 a 65 536 ports d’E/S, et les instructions in et out :
in al, 0x60 ; e4 60 read a byte from port 0x60
out 0x80, al ; e6 80 write a byte to port 0x80
mov dx, 0x3f8 ; 66 ba f8 03
out dx, al ; ee port number in dx: 0x3f8 is the first serial port
in al, dx ; ec
(Encodages obtenus avec clang -target x86_64-linux-gnu.) Sur le bus, le CPU indique que l’adresse est un port d’E/S et non une adresse mémoire par une ligne de contrôle dédiée (M/IO# sur les puces x86, IORQ# sur le Z80), si bien que les puces mémoire ignorent le cycle et que les circuits d’E/S répondent. Le port 0x80 a une curieuse seconde vie : le firmware y écrit un code d’avancement pendant le démarrage, qu’une carte de diagnostic branchée sur la carte mère peut afficher.
Le simulateur de ce site n’implémente pas in et out. C’est fidèle à sa façon : un programme normal ne peut pas les exécuter non plus. En mode utilisateur, elles déclenchent une faute de protection générale, que Linux transforme en SIGSEGV, sauf si le noyau a explicitement accordé au processus l’accès à ces ports (appels système ioperm et iopl). Toucher au matériel est exactement le genre de chose qui doit passer par le noyau, comme l’explique le chapitre sur les appels système.
Les E/S projetées en mémoire (memory-mapped I/O) placent les registres des périphériques dans l’espace d’adressage ordinaire. Un chargement ou un rangement à la bonne adresse atteint le périphérique au lieu de la RAM. ARM et RISC-V n’ont que cela, et sur x86 presque tous les périphériques modernes, tout ce qui est sur PCI Express, sont projetés en mémoire eux aussi ; l’espace des ports survit pour les périphériques hérités. L’avantage est que n’importe quelle instruction et n’importe quel pointeur peuvent atteindre un périphérique. En C, le pointeur doit être déclaré volatile, pour que le compilateur effectue chaque accès exactement comme il est écrit, sans sauter une lecture dont il croit connaître la valeur, sans fusionner deux écritures :
#define PL061_BASE 0x20060000UL
#define GPIODIR (*(volatile unsigned int *)(PL061_BASE + 0x400))
#define GPIODATA(m) (*(volatile unsigned int *)(PL061_BASE + ((m) << 2)))
void led_on(void) {
GPIODIR |= 1u << 3; /* line 3 is an output */
GPIODATA(1u << 3) = 0xff; /* set line 3; the address masks the other 7 */
}
Avec clang -O2, cela devient de simples chargements et rangements, les mêmes instructions que pour la mémoire. Pour ARM64 :
mov w8, #1024
mov w10, #255
movk w8, #8198, lsl #16 ; x8 = 0x20060400, GPIODIR
ldr w9, [x8]
sub x11, x8, #992 ; x11 = 0x20060020, GPIODATA with mask 0b00001000
orr w9, w9, #0x8
str w9, [x8]
str w10, [x11]
ret
Et pour x86-64, où une seule instruction peut lire, modifier et écrire la mémoire :
or dword ptr [537265152], 8
mov dword ptr [537264160], 255
ret
537 265 152, c’est 0x20060400, le registre de sens, et 537 264 160, c’est 0x20060020. Le matériel doit coopérer aussi : les registres de périphériques ne doivent pas passer par les caches, et le CPU ne doit ni réordonner ni fusionner leurs accès. Les tables de pages marquent ces zones avec des types de mémoire spéciaux, non cachable (uncacheable) sur x86, mémoire Device sur ARM, qui le garantissent.
Le PL061 utilisé ici est un vrai contrôleur GPIO conçu par ARM, et son registre de données montre un joli usage des lignes d’adresse. Il occupe les adresses 0x000 à 0x3FC, et les bits 9–2 de l’adresse servent de masque : une écriture ne change que les lignes dont le bit de masque vaut 1. Écrire 0xff à l’offset 0x20 (masque 0b00001000) met la ligne 3 à 1 et laisse les sept autres tranquilles, sans avoir besoin de lire le registre d’abord.
Le décodage d’adresses
Chaque puce d’un bus ne doit répondre qu’à ses propres adresses. Le circuit qui en décide est le décodeur d’adresse, qui pilote l’entrée de sélection de puce (CS, chip select) de chaque puce. Prenons un petit ordinateur avec un bus d’adresse de 16 bits (64 Kio) et quatre puces :
| Adresses | A15 A14 | Puce |
|---|---|---|
| 0x0000–0x3FFF | 0 0 | ROM, 16 Kio : le programme |
| 0x4000–0x7FFF | 0 1 | RAM 0, 16 Kio |
| 0x8000–0xBFFF | 1 0 | RAM 1, 16 Kio |
| 0xC000–0xFFFF | 1 1 | circuit d’E/S |
Les deux bits d’adresse de poids fort choisissent la puce ; les 14 bits de poids faible vont à toutes les puces et désignent un octet à l’intérieur. Le décodeur est le décodeur 2 vers 4 avec une entrée de plus, MREQ, qui indique que le CPU fait réellement un accès mémoire à cet instant :
À essayer : Cliquez sur un interrupteur d’entrée du circuit (ou sur son bouton au-dessus) pour le basculer. Les portes et la table de vérité suivent.
| A15 | A14 | MREQ | CS ROM | CS RAM0 | CS RAM1 | CS I/O |
|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 0 | 0 | 1 | 1 | 0 | 0 | 0 |
| 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| 0 | 1 | 1 | 0 | 1 | 0 | 0 |
| 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 1 | 0 | 0 | 1 | 0 |
| 1 | 1 | 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 0 | 0 | 0 | 1 |
The two highest address bits split a 64 KiB address space into four 16 KiB blocks, and a 2-to-4 decoder turns them into one chip select per block: 0000–3FFF ROM, 4000–7FFF and 8000–BFFF RAM, C000–FFFF an I/O chip. MREQ is the enable: no chip is selected unless the CPU is actually accessing memory. The low 14 address bits go to every chip, which decodes them internally.
Unit-delay model: every gate takes one step to react. With auto off, toggle switches and press step to watch the change travel gate by gate.
Essayez les quatre combinaisons de A15 et A14 : une seule sélection de puce s’allume à chaque fois. Coupez MREQ et plus aucune ne s’allume, quelle que soit l’adresse. Le bus d’adresse peut porter n’importe quoi entre deux cycles de bus, et aucune puce ne doit y répondre.
Voyons maintenant pourquoi MREQ compte. Réglez A15 = A14 = 0 (ROM sélectionnée), désactivez auto, et passez A15 à 1 en laissant MREQ activé. Pendant un délai de porte, CS ROM et CS RAM1 sont allumés ensemble : la nouvelle sélection monte avant que l’inverseur ait éteint l’ancienne. Si les deux puces pilotaient le bus de données à cet instant, elles se battraient. C’est pourquoi les spécifications de bus exigent que l’adresse soit stable avant l’activation de MREQ (le temps de prépositionnement d’adresse T_ML du chapitre sur les chronogrammes), et pourquoi les décodeurs sont validés par MREQ.
Les vrais systèmes utilisent exactement ce genre de puce. Le classique 74HC138 est un décodeur 3 vers 8 avec trois entrées de validation ; ses sorties sont actives à l’état bas, comme le sont en général les sélections de puce.
Décodage complet et partiel
Le circuit d’E/S a peut-être quatre registres, choisis par A1 et A0. Mais le décodeur lui donne les 16 Kio de 0xC000 à 0xFFFF. Les bits A13 à A2 sont ignorés, donc la puce répond à toute adresse dont les deux bits de poids faible désignent un registre : ses quatre registres apparaissent 16 384 / 4 = 4096 fois, en 0xC000, 0xC004, 0xC008 et ainsi de suite. C’est le décodage partiel. Il est bon marché et sans danger tant que rien d’autre n’a besoin de ces adresses ; il pose problème quand le système doit grandir. Le décodage complet comparerait les 14 bits supérieurs (A15–A2), pour que la puce n’apparaisse qu’une fois.
Un décodeur encore plus économique illustre la même idée : si la seule puce de la moitié basse de la mémoire est l’EPROM, sa sélection de puce peut simplement être reliée à A15, et l’EPROM répond sur 32 Kio d’adresses.
Des décodeurs programmables : les BAR de PCI
Un PC ne peut pas figer sa carte d’adresses dans le câblage, parce que personne ne sait à l’avance quelles cartes seront installées. Chaque périphérique PCI ou PCIe contient donc ses propres comparateurs d’adresse, que le système d’exploitation, ou le firmware, programme. Ce sont les registres d’adresse de base (BAR, base address registers) de l’espace de configuration du périphérique, aux offsets 0x10 à 0x27. Voici les premiers de la carte réseau virtio de la VM Linux du chapitre sur PCI Express :
$ head -c 64 /sys/bus/pci/devices/0000:00:01.0/config | od -A x -t x1
000000 f4 1a 41 10 06 00 10 00 01 00 00 02 00 40 00 00
000010 04 00 00 80 02 00 00 00 00 01 00 50 00 00 00 00
…
$ head -3 /sys/bus/pci/devices/0000:00:01.0/resource
0x0000000280000000 0x000000028000ffff 0x0000000000140204
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000050000100 0x000000005000013f 0x0000000000040200
Lus comme des mots de 32 bits en petit-boutiste :
- BAR0 = 0x80000004. Le bit 0 vaut 0 : un BAR mémoire, pas un BAR de ports d’E/S. Les bits 2–1 valent 10 : une adresse de 64 bits, dont la moitié haute est dans BAR1 = 0x00000002. Ensemble : le périphérique décode les adresses à partir de 0x2_8000_0000, et le fichier
resourcede Linux montre la plage, 64 Kio jusqu’à 0x2_8000_FFFF. - BAR2 = 0x50000100 : un BAR mémoire de 32 bits en 0x5000_0100, long de 64 octets.
- Le registre de commande à l’offset 4 vaut 0x0006 : le bit 1 active les décodeurs mémoire du périphérique, et le bit 2 l’autorise à devenir maître du bus, pour le DMA.
La taille n’est stockée nulle part de façon visible. Le firmware la trouve en écrivant des 1 partout dans un BAR puis en le relisant : le périphérique laisse à 0 les bits de poids faible que son comparateur ignore, et le nombre de bits à zéro donne la taille. Il y écrit ensuite l’adresse de base choisie. Le BAR, c’est le décodeur d’adresse de la section précédente, rendu programmable, et c’est ainsi que deux cartes de fabricants différents reçoivent automatiquement des adresses qui ne se chevauchent pas.
Les périphériques intégrés à une puce n’ont pas besoin de BAR : leurs adresses sont fixées par les concepteurs de la puce et décrites au système d’exploitation par le firmware. La VM Linux en a deux, nommés d’après leur adresse :
$ ls /sys/bus/amba/devices
20050000.pl031
20060000.pl061
$ cat /sys/bus/amba/devices/20060000.pl061/resource /sys/bus/amba/devices/20060000.pl061/id
0000000020060000 0000000020060fff 0000000000000200
00041061
Une horloge temps réel PL031 en 0x2005_0000 et un contrôleur GPIO PL061 (celui de l’exemple en C plus haut) en 0x2006_0000, sur 4 Kio. L’identifiant 0x00041061 contient le code d’ARM (0x41) et le numéro de pièce (0x061). La carte complète de la machine se trouve dans /proc/iomem, mais dans le conteneur chaque plage se lit 00000000-00000000 : le noyau cache les adresses physiques aux processus qui n’ont pas les privilèges d’administration.
Les GPIO des microcontrôleurs
Sur un microcontrôleur, les broches d’E/S à usage général (GPIO, general-purpose I/O) sont le principal moyen pour le programme de toucher le monde : boutons, LED, relais, le contact de porte d’un four à micro-ondes. L’ATmega168 du chapitre sur les CPU contrôle chaque port avec trois registres :
- DDRx (data direction) : un 1 fait de la broche une sortie ;
- PORTx : la valeur pilotée sur les broches de sortie (ou, sur les entrées, l’activation d’une résistance de rappel) ;
- PINx : le niveau actuel des broches, en lecture seule.
Sur un Arduino Uno, la LED intégrée est sur la broche 5 du port B. L’allumer prend deux lignes de C, avec les registres à leurs adresses en mémoire de données, 0x24 pour DDRB et 0x25 pour PORTB :
#define DDRB (*(volatile unsigned char *)0x24)
#define PORTB (*(volatile unsigned char *)0x25)
void led_on(void) { DDRB |= 1 << 5; PORTB |= 1 << 5; }
Compilé avec le générateur de code AVR de LLVM pour l’ATmega168, cela donne :
led_on:
sbi 4, 5 ; set bit 5 of I/O register 4 (DDRB)
sbi 5, 5 ; set bit 5 of I/O register 5 (PORTB)
ret
L’AVR a les deux mécanismes à la fois. Ses 64 premiers registres d’E/S sont projetés en mémoire aux adresses de données 0x20 à 0x5F, et sont aussi accessibles comme adresses d’E/S 0x00 à 0x3F avec les instructions spéciales in et out, et, pour les 32 premiers, avec sbi et cbi, qui mettent à 1 ou à 0 un seul bit. Le compilateur a vu les adresses mémoire 0x24 et 0x25, les a reconnues comme les registres d’E/S 4 et 5, et a utilisé sbi, qui met un bit à 1 en une instruction. Cela évite au passage un bogue subtil : la lecture-modification-écriture de PORTB |= … faite en trois instructions pourrait être interrompue au milieu par un gestionnaire d’interruption qui modifie un autre bit du même port, et cette modification serait alors perdue.
Les bus série entre puces
La plupart des puces d’une carte ne sont pas du tout sur le bus mémoire du CPU. Elles sont reliées à de simples bus série, pilotés par des blocs contrôleurs à l’intérieur du SoC ou du microcontrôleur :
| Bus | Fils | Horloge | Choix du périphérique | Vitesses typiques |
|---|---|---|---|---|
| UART | TX, RX | aucune : les deux côtés s’accordent sur le débit ; bits de start et de stop autour de chaque octet | point à point | 9600 à 115 200 bauds classiquement, des mégabauds aujourd’hui |
| SPI | horloge, données sortantes, données entrantes, plus une sélection de puce par périphérique | pilotée par le contrôleur | un fil CS séparé par périphérique : le décodage d’adresse par le câblage | des dizaines de MHz |
| I²C | SDA (données), SCL (horloge), toutes deux à drain ouvert avec résistances de rappel | pilotée par le contrôleur, étirable par le périphérique | une adresse de 7 bits envoyée au début de chaque transfert | 100 kHz, 400 kHz, 1 MHz, 3,4 MHz |
C’est par SPI qu’un PC lit son firmware dans la puce de flash NOR à la mise sous tension ; c’est par I²C et son proche parent SMBus qu’il lit la puce SPD de chaque module mémoire et parle aux capteurs de température et aux régulateurs de tension. Sélection de puce, décodage d’adresse et poignées de main sont tous encore là, simplement étalés dans le temps sur deux ou quatre fils.
Comment un pilote parle à un périphérique
Pour tout rassembler, voici ce que fait le pilote d’un système d’exploitation avec un périphérique PCI Express :
- Le trouver : au démarrage, le noyau lit l’espace de configuration de chaque adresse de périphérique possible. Un identifiant de fabricant 0xFFFF signifie qu’il n’y a rien ; sinon, les identifiants de fabricant et de modèle, et le code de classe, désignent le pilote.
- Lui donner des adresses : le firmware ou le noyau mesure et programme les BAR, puis met à 1 les bits d’activation mémoire et de maître de bus du registre de commande.
- Projeter les registres : les adresses physiques du BAR sont projetées dans l’espace d’adressage virtuel du noyau comme mémoire de périphérique non cachable (
ioremapsous Linux). - Dialoguer : le pilote lit et écrit les registres avec des fonctions comme
readletwritelde Linux : des accès volatils accompagnés des barrières mémoire dont l’architecture a besoin. Les données en volume ne passent pas par les registres : le pilote donne au périphérique l’adresse d’un tampon en RAM, et le périphérique le transfère par DMA. - Écouter : le périphérique signale la fin d’une opération par une interruption, une écriture mémoire via MSI, qui exécute le gestionnaire du pilote.
Les programmes utilisateur ne voient rien de tout cela. Ils appellent read et write sur un descripteur de fichier, et les pilotes du noyau transforment ces appels en accès aux registres. C’est la frontière tracée dans le chapitre sur les appels système, et le sujet du chapitre sur les fichiers, les périphériques et les E/S.
À retenir
- Un circuit d’E/S, ce sont quelques registres de données, d’état et de contrôle. Les UART (la famille 8250/16550) et les PIO (le 8255A) sont les classiques ; les UART sont encore présents sur chaque microcontrôleur et sur la plupart des cartes.
- Les E/S par ports utilisent un espace d’adressage séparé et des instructions spéciales (
in/outsur x86, que les programmes utilisateur ne peuvent pas exécuter). Les E/S projetées en mémoire utilisent des chargements et rangements ordinaires vers des adressesvolatilenon cachables ; ARM, RISC-V et presque tous les périphériques modernes s’en servent. - Un décodeur d’adresse transforme les bits d’adresse de poids fort en sélections de puce, validées par MREQ pour que rien ne réponde pendant que l’adresse change. Le décodage partiel fait apparaître une puce de nombreuses fois ; le décodage complet compare tous les bits d’adresse.
- Les registres d’adresse de base de PCI sont des décodeurs d’adresse programmables : mesurés en y écrivant des 1, réglés par le firmware ou le système. Les périphériques intégrés ont des adresses fixes, comme le GPIO PL061 en 0x2006_0000 dans la VM Linux.
- Les microcontrôleurs pilotent leurs GPIO par des registres de sens, de sortie et d’entrée ; l’AVR les atteint à la fois comme mémoire et comme ports d’E/S (
sbi). Les puces d’une carte dialoguent par UART, SPI et I²C, et un pilote relie le tout : espace de configuration, BAR, registres projetés, DMA et interruptions.