Aller au contenu principal

Agir sur le monde

Ce que ce chapitre apporte

  • Expliquer ce qu'est une entrée-sortie mappée en mémoire, et pourquoi elle ne demande aucune instruction nouvelle.
  • Allumer, éteindre et faire clignoter une sortie physique en écrivant dans une case, à l'aide de sb et d'un décalage.
  • Commander un afficheur qui occupe un mot entier, et distinguer son accès de celui d'une sortie d'un octet.
  • Traduire un appel comme digitalWrite() en la suite d'instructions qu'il exécute réellement.
  • Lire un registre d'état matériel et en extraire un bit, pour décider de la suite à partir de ce que le monde répond.

Jusqu'ici, les programmes de ce cours ont rangé des valeurs dans des cases dont personne ne relira jamais le contenu. Un vrai programme fait autre chose : il allume un voyant, pilote une vanne, met un caractère à l'écran. La question mérite d'être posée franchement, parce que sa réponse est contre-intuitive. Il n'existe aucune instruction qui affiche, aucune qui allume, aucune qui commande. Le jeu d'instructions de ce chapitre est exactement celui du précédent, sw et sb compris, et rien de plus. Ce qui change, c'est qu'un morceau de matériel a été câblé pour surveiller certaines adresses, et pour réagir à ce qui s'y écrit.

Une lampe est une case que quelqu'un regarde

Le principe tient en une phrase. Le concepteur d'une carte décide qu'une certaine adresse ne correspond pas à de la mémoire ordinaire, mais à un circuit. Quand le processeur écrit à cette adresse, le circuit reçoit la valeur et fait ce pour quoi il a été fabriqué : fermer un relais, allumer une diode, envoyer un octet sur une liaison série. Pour le processeur, absolument rien ne change. Il exécute un sb, comme la veille sur une case de données.

Cette organisation porte un nom, l'entrée-sortie mappée en mémoire, et elle est très largement majoritaire aujourd'hui. Elle explique pourquoi un plan de mémoire n'est pas un espace uniforme : une zone est de la mémoire vive, une autre de la mémoire morte, une troisième est une fenêtre sur un contrôleur. La documentation d'un microcontrôleur consacre l'essentiel de ses pages à cette carte des adresses.

Dans ce cours, la ligne matériel: câble le périphérique.

RISC-V RV32Ipas 0 sur 4pc = 0x0

programme

  1. 0x0la t0, rampelui t0, 3
  2. 0x4addi zero, zero, 0
  3. 0x8li t1, 1addi t1, zero, 1
  4. 0xcsb t1, 0(t0)

registres

sp0x2000
t00
t10

mémoire

rampe0x3000

Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.

Exécuter les quatre pas et surveiller la rampe de lampes affichée sous la mémoire : elle reste éteinte jusqu'au dernier pas, où la première lampe s'allume au moment exact où la figure signale l'écriture à l'adresse 0x3000.

La déclaration pose huit lampes à partir de 0x3000, chacune occupant un octet. Le programme est celui du chapitre précédent, au mot près : charger l'adresse, poser une valeur dans un registre, ranger cet octet. La seule nouveauté est visuelle, et elle est de taille : la figure montre une lampe s'allumer.

Aucune instruction ne sait allumer quoi que ce soit

sb ne sait rien des lampes. Il recopie un octet à une adresse, ni plus ni moins, et il ferait exactement la même chose si cette adresse désignait une case de données inerte. C'est le câblage qui donne son effet à l'écriture. Retirer le circuit et le même programme ne fait plus rien, sans qu'aucune instruction ait changé.

Éteindre, et faire clignoter

Si écrire une valeur non nulle allume, écrire zéro éteint. Le registre zero sert alors directement de source.

RISC-V RV32Ipas 0 sur 7pc = 0x0

programme

  1. 0x0la t0, rampelui t0, 3
  2. 0x4addi zero, zero, 0
  3. 0x8li t1, 1addi t1, zero, 1
  4. 0xcsb t1, 0(t0)
  5. 0x10sb zero, 0(t0)
  6. 0x14sb t1, 0(t0)
  7. 0x18sb zero, 0(t0)

registres

sp0x2000
t00
t10

mémoire

rampe0x3000

Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.

Avancer pas à pas à partir du quatrième et regarder la première lampe s'allumer, s'éteindre, se rallumer et s'éteindre. Reculer ensuite d'un pas pour la rallumer, ce qui montre que l'état de la lampe n'est rien d'autre que le contenu de la case.

Quatre écritures à la même adresse, et le clignotement est là. Chaque sb remplace le contenu de la case, et la lampe suit. La figure signale d'ailleurs, à chaque écriture, la valeur qui se trouvait là avant : 0 puis 1 puis 0, en alternance.

Une remarque s'impose sur ce programme, parce qu'elle vaut pour tout pilotage réel. Le clignotement est ici instantané et invisible sur une carte véritable : quatre instructions s'exécutent en quelques nanosecondes, et aucun œil ne verrait la diode s'éteindre. Un vrai programme de clignotement passe l'essentiel de son temps à attendre, soit en comptant dans une boucle, soit en interrogeant une horloge matérielle, qui est elle aussi une case à une adresse connue. Le chapitre 6 donnera la boucle ; le principe de l'attente, lui, est déjà entier : lire une case jusqu'à ce qu'elle change.

Le décalage désigne la sortie

Les huit lampes occupent huit octets consécutifs. Leur atteindre ne demande rien de plus que le décalage déjà vu sur un tableau de données.

RISC-V RV32Ipas 0 sur 7pc = 0x0

programme

  1. 0x0la t0, rampelui t0, 3
  2. 0x4addi zero, zero, 0
  3. 0x8li t1, 1addi t1, zero, 1
  4. 0xcsb t1, 0(t0)
  5. 0x10sb t1, 2(t0)
  6. 0x14sb t1, 4(t0)
  7. 0x18sb t1, 6(t0)

registres

sp0x2000
t00
t10

mémoire

rampe0x3000

Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.

Exécuter jusqu'au bout et compter les lampes allumées : une sur deux, aux rangs 0, 2, 4 et 6. Vérifier dans la ligne d'accès mémoire que les quatre adresses touchées sont 0x3000, 0x3002, 0x3004 et 0x3006.

Une seule adresse a été chargée, et quatre sorties distinctes ont été commandées. Le décalage joue exactement le rôle qu'il jouait sur le tableau mesures du chapitre précédent : il désigne un élément à partir d'une base. Un contrôleur industriel qui commande seize vannes fonctionne ainsi, à ceci près que les vannes sont souvent regroupées dans les bits d'un même octet, ce qui est le sujet du chapitre suivant.

Le décalage n'est pas obligé d'être écrit dans l'instruction. Il peut venir d'un registre, et c'est là que le motif devient reconnaissable.

RISC-V RV32Ipas 0 sur 5pc = 0x0

programme

  1. 0x0la t0, sortieslui t0, 3
  2. 0x4addi zero, zero, 0
  3. 0x8add t0, t0, a0
  4. 0xcli t1, 1addi t1, zero, 1
  5. 0x10sb t1, 0(t0)

registres

sp0x2000
t00
t10
a05

mémoire

sorties0x3000

Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.

Relever la valeur de t0 après le troisième pas, 12293, et la comparer à l'adresse touchée par l'écriture finale, 0x3005. Changer mentalement a0 en 2 et prévoir quelle lampe s'allumerait.

Ce programme mérite d'être lu deux fois, parce qu'il est la traduction d'une ligne que beaucoup ont déjà écrite. a0 contient un numéro de sortie, ici 5. Le programme ajoute ce numéro à l'adresse de base du périphérique, puis écrit 1 à l'adresse obtenue.

Ce que fait vraiment `digitalWrite(5, HIGH)`

L'appel n'est pas une primitive du monde. C'est une fonction, écrite par quelqu'un, qui contient une table associant un numéro de broche à une adresse de registre matériel et à un numéro de bit, puis qui effectue une écriture à cette adresse. Les cinq instructions de la figure précédente en sont la version dépouillée. Rien d'autre ne se produit dans la machine : pas d'appel au système, pas de couche cachée, une écriture.

Le même raisonnement vaut au-delà de l'Arduino. Un pilote de périphérique, sous n'importe quel système, est un morceau de code qui connaît des adresses et écrit dedans. Le mot « pilote » évoque une machinerie complexe ; l'essentiel de son travail tient dans des sw et des lw à des adresses documentées, plus la discipline nécessaire pour les faire dans le bon ordre.

Un afficheur occupe un mot

Toutes les sorties ne tiennent pas dans un octet. Un afficheur numérique reçoit un nombre entier, et se commande donc par un mot de quatre octets.

RISC-V RV32Ipas 0 sur 8pc = 0x0

programme

  1. 0x0la t0, compteurlui t0, 3
  2. 0x4addi t0, t0, 16
  3. 0x8li t1, 0addi t1, zero, 0
  4. 0xcsw t1, 0(t0)
  5. 0x10addi t1, t1, 1
  6. 0x14sw t1, 0(t0)
  7. 0x18addi t1, t1, 1
  8. 0x1csw t1, 0(t0)

registres

sp0x2000
t00
t10

mémoire

compteur0

Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.

Avancer pas à pas et lire l'afficheur de la figure après chacune des trois écritures : il indique 0, puis 1, puis 2. Remarquer que les deux instructions addi ne changent rien à l'affichage tant qu'aucun sw n'a eu lieu.

La dernière observation est celle qui compte. addi t1, t1, 1 incrémente un registre, et l'afficheur ne bouge pas. Il ne bouge qu'au sw suivant. Le compteur vit dans t1, l'affichage vit dans la case 0x3010, et les deux ne sont reliés que par les instructions qui recopient l'un vers l'autre. C'est la distinction entre un état interne et une sortie, et c'est elle qui explique pourquoi une interface reste figée pendant qu'un calcul avance.

Rien n'empêche de commander plusieurs périphériques dans le même programme : ce ne sont que des adresses différentes.

RISC-V RV32Ipas 0 sur 11pc = 0x0

programme

  1. 0x0la t0, rampelui t0, 3
  2. 0x4addi zero, zero, 0
  3. 0x8la t1, compteurlui t1, 3
  4. 0xcaddi t1, t1, 16
  5. 0x10li t2, 1addi t2, zero, 1
  6. 0x14sb t2, 0(t0)
  7. 0x18li t3, 1addi t3, zero, 1
  8. 0x1csw t3, 0(t1)
  9. 0x20sb t2, 1(t0)
  10. 0x24addi t3, t3, 1
  11. 0x28sw t3, 0(t1)

registres

sp0x2000
t00
t10
t20
t30

mémoire

rampe0x3000
compteur0

Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.

Exécuter jusqu'au bout en gardant les deux périphériques sous les yeux : chaque lampe qui s'allume est suivie d'une mise à jour de l'afficheur, qui finit à 2. Vérifier que les adresses touchées alternent entre les octets de la rampe, 0x3000 puis 0x3001, et l'adresse de l'afficheur, 0x3010.

Deux registres tiennent les deux adresses de base, et le reste du programme n'est qu'une suite d'écritures. Un système embarqué sérieux fait exactement cela, avec quelques dizaines de périphériques et des adresses moins rondes.

Écrire à la bonne largeur

La largeur de l'écriture n'est pas un détail de style. Elle décide du nombre d'octets touchés, donc du nombre de sorties affectées.

RISC-V RV32Ipas 0 sur 8pc = 0x0

programme

  1. 0x0la t0, rampelui t0, 3
  2. 0x4addi zero, zero, 0
  3. 0x8li t1, 1addi t1, zero, 1
  4. 0xcsb t1, 0(t0)
  5. 0x10sb t1, 1(t0)
  6. 0x14sb t1, 2(t0)
  7. 0x18sb t1, 3(t0)
  8. 0x1csw t1, 0(t0)

registres

sp0x2000
t00
t10

mémoire

rampe0x3000

Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.

Compter les lampes allumées après le septième pas, quatre, puis exécuter le dernier pas et les recompter : il n'en reste qu'une. Regarder la ligne d'accès mémoire du dernier pas, qui annonce quatre octets écrits à partir de 0x3000.

Les quatre premières lampes sont allumées une par une. La dernière instruction, sw t1, 0(t0), range la valeur 1 sur quatre octets : l'octet de poids faible vaut 1, les trois autres valent 0. Trois lampes s'éteignent donc, sans qu'aucune instruction n'ait prétendu les éteindre.

Une écriture large éteint les voisins en silence

C'est l'une des erreurs les plus fréquentes dans le pilotage de matériel, et l'une des plus difficiles à voir en relisant le code, parce que la ligne fautive a l'air correcte : elle écrit la bonne valeur, à la bonne adresse, dans la mauvaise largeur. Le symptôme se manifeste ailleurs, sur un périphérique voisin qui cesse de répondre. La règle de conduite est simple : une largeur d'accès se lit dans la documentation du composant, elle ne se devine pas.

L'autre versant de la même discipline concerne l'alignement, déjà rencontré sur les données.

RISC-V RV32Ipas 0 sur 4pc = 0x0

programme

  1. 0x0la t0, compteurlui t0, 3
  2. 0x4addi t0, t0, 16
  3. 0x8addi t0, t0, 2
  4. 0xcli t1, 7addi t1, zero, 7
  5. 0x10sw t1, 0(t0)

registres

t00
t10

mémoire

compteur0

Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.

Exécuter jusqu'à l'arrêt : la figure s'interrompt avant d'écrire et affiche « sw veut une adresse multiple de 4, et 0x3012 ne l'est pas ». L'afficheur reste à 0, ce qui est la seule bonne nouvelle de ce programme.

Un addi de trop sur l'adresse de base, et l'accès est refusé. Sur une carte réelle, l'issue est parfois pire qu'un refus : selon le composant, une écriture décalée atteint un registre voisin qui n'attendait rien de tel.

Le monde répond aussi

Écrire commande, lire renseigne. Un contrôleur dispose presque toujours d'un registre d'état, à une adresse documentée, dans lequel il dépose ce qu'il a à dire : la donnée est prête, le tampon est plein, un défaut est survenu. Chaque information tient dans un bit, parce qu'un bit suffit à dire oui ou non et qu'un contrôleur n'a pas de place à gaspiller.

RISC-V RV32Ipas 0 sur 5pc = 0x0

programme

  1. 0x0la t0, etatlui t0, 3
  2. 0x4addi t0, t0, 256
  3. 0x8lb t1, 0(t0)
  4. 0xcsrli t2, t1, 2
  5. 0x10andi t3, t2, 1

registres

sp0x2000
t00
t10
t20
t30

mémoire

0x3100etat13

Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.

Relever les trois valeurs successives, 13 puis 3 puis 1, et comparer l'écriture binaire de la valeur de départ, 00001101, à celle de la valeur intermédiaire, 11. Le résultat final vaut 1 : le bit de rang 2 est à un.

La ligne données: etat @ 0x3100 tient lieu ici de ce qu'un contrôleur aurait déposé à cette adresse. Le point n'est pas de tricher : de la mémoire ordinaire et un registre matériel se lisent avec la même instruction, et c'est précisément ce que ce chapitre affirme depuis le début.

Le programme lit l'octet d'état, qui vaut 13. Reste à en extraire un seul bit, celui de rang 2. srli t2, t1, 2 décale le contenu de deux positions vers la droite, ce qui amène le bit voulu en dernière position : 13 devient 3. andi t3, t2, 1 ne garde que cette dernière position, et donne 1.

Ces deux instructions sont la matière du chapitre suivant, et elles sont données ici sans démonstration, parce qu'aucun dialogue avec du matériel ne se passe d'elles. Un pilote lit un octet d'état, isole un bit, et décide. La décision elle-même viendra au chapitre 5 ; l'isolement du bit vient tout de suite après.

Lire un registre matériel n'est pas anodin

Sur un composant réel, la lecture d'un registre d'état modifie parfois ce registre : certains drapeaux s'effacent au moment où ils sont lus, précisément pour signaler qu'ils ont été vus. Deux lectures successives ne rendent alors pas la même valeur, ce qui est impensable pour une case de mémoire ordinaire. Une variable qui change sans qu'aucune instruction ne l'écrive est le genre de phénomène qu'aucun langage de haut niveau ne prépare à rencontrer.

Vérification rapideon peut se reprendre

1.Quelle instruction allume une lampe câblée à l'adresse 0x3000 ?

2.Dans la figure des huit lampes, à quoi sert le décalage de sb t1, 4(t0) ?

3.Que fait réellement digitalWrite(5, HIGH) sur une carte ?

4.Un programme incrémente un registre trois fois sans aucun sw. Que montre l'afficheur ?

5.Quatre lampes voisines sont allumées, puis un sw range la valeur 1 à l'adresse de la première. Que se passe-t-il ?

6.Pourquoi un registre d'état matériel se lit-il souvent bit par bit ?

La méthode

  1. Chercher l'adresse dans la documentation du composant avant d'écrire la moindre instruction : une carte des adresses et une largeur d'accès sont les deux seules informations indispensables.
  2. Charger l'adresse de base une fois, dans un registre réservé au périphérique, et désigner les sorties voisines par un décalage plutôt que par un nouveau calcul.
  3. Choisir la largeur d'après le composant : sb pour une sortie d'un octet, sw pour un registre ou un afficheur d'un mot, et vérifier qu'aucun voisin ne se trouve dans les octets emportés.
  4. Contrôler l'alignement de toute adresse obtenue par un calcul, avant l'accès plutôt qu'après la panne.
  5. Séparer l'état interne de la sortie : garder le compteur, la consigne ou la mesure dans un registre, et ne recopier vers le périphérique qu'au moment voulu.
  6. Lire le registre d'état avant de conclure qu'un périphérique ne répond pas, et isoler le bit qui renseigne plutôt que de comparer l'octet entier.
  7. Se rappeler qu'une lecture peut avoir un effet sur un composant réel, et ne pas relire un registre d'état par confort.

Synthèse

  • Une entrée-sortie mappée en mémoire est une adresse surveillée par un circuit. Écrire dedans agit sur le monde, et cela se fait avec les instructions déjà connues, sb et sw, sans aucun ajout au jeu d'instructions.
  • Une lampe s'allume sur une valeur non nulle et s'éteint sur zéro : le registre zero sert directement de source à l'extinction, et l'état visible n'est rien d'autre que le contenu de la case.
  • Le décalage désigne la sortie. Une seule adresse de base chargée avec la suffit à commander huit lampes, et un décalage venu d'un registre transforme un numéro de sortie en adresse.
  • digitalWrite() n'est pas une primitive du monde : c'est une fonction qui traduit un numéro de broche en adresse, puis écrit. Tout pilote de périphérique est fait de cette matière.
  • Un afficheur occupe un mot et se commande par sw, tandis qu'une lampe occupe un octet et se commande par sb. Une écriture trop large éteint les voisins en silence, et une adresse mal alignée fait échouer l'accès.
  • Un registre d'état se lit, et chacun de ses bits porte une information indépendante. L'extraire demande un décalage et un masque, ce que srli et andi font en deux instructions.
  • Un registre du processeur et une sortie physique sont deux choses distinctes : incrémenter le premier ne change rien au second tant qu'aucune écriture ne les relie.

Ces deux instructions employées à la fin, srli et andi, ont été admises sans justification. Masquer et filtrer les reprend depuis le début, avec tout ce qui permet d'isoler un champ, de forcer un drapeau et de basculer un bit sans toucher aux autres.