Aller au contenu

Interruptions (TP 4)

Introduction

Jusqu'à maintenant, notre architecture vit dans son monde, elle est quasiment incapable de s'interfacer avec le monde extérieur. Nous avons en vérité déjà vus quelques sorties avec les afficheurs 7 segments mais elle n'est pas sensible à des entrées. On va ici étudier comment ajouter des périphériques d'entrée et être capable de répondre à des événements asynchrones (e.g. l'appui sur un bouton) produits lorsque ces périphériques sont utilisés. On va voir notamment:

  • le principe des interruptions
  • le codage des gestionnaires d'interruption (interrupt handlers)
  • la gestion d'une interruption pendant l'exécution d'un programme

L'architecture que je vous propose d'utiliser est représentée ci-dessous. Vous y verrez notamment l'ajout d'un registre (Interrupt Flag) ainsi que trois signaux de contrôle INTA, SetIF, ClearIF. Vous y verrez également un petit circuit (à côté de la RAM) permettant de générer une interruption lors de l'appui sur un bouton et une modification du contrôle du multiplexeur du MicroPC. Téléchargez le fichier archi_irq.circ ainsi que le fichier csmetz.jar à placer dans le même répertoire que archi_irq.circ. Ouvrez le circuit avec logisim. Chargez la ROM microcode_irq.rom qui contient les micro-instructions des instructions introduites dans le sujet précédent.

Je vous rappelle que vous disposez de la carte de référence de l'architecture.

Architecture avec la gestion des interruptions

Le principe des interruptions

Le registre Interrupt Flag est là pour autoriser ou non les interruptions. En effet, si, par exemple, le pointeur de pile n'est pas encore initialisé, il ne faut surtout pas partir en interruption puisque c'est justement sur la pile qu'on mémorise le contenu des registres en quittant la routine d'interruption. Au démarrage de la machine, ce registre est à 0, on s'assurera d'autoriser les interruptions après l'initialisation. On introduit deux instructions pour gérer l'état de ce registre ainsi que deux instructions pour partir en interruption et revenir d'une interruption.

Code Opération (8 bits) Nom de l'opération Nombre de mots Description
0xd0 CLI 1 Met à zéro le registre Interrupt Flag (IF).

IF := 0.
0xd4 STI 1 Met à un le registre Interrupt Flag (IF).

IF := 1.
0xe0 INT 1 Départ en interruption. Le vecteur d'interruption doit être en RAM à l'adresse 0x0002. Cette adresse est chargeable dans le PC via le signal de contrôle INTA qui accuse réception de l'interruption.
0xe8 RTI 1 Retour d'une interruption en reprenant le déroulement du programme interrompu.

On pourrait appeler depuis un programme l'interruption (en invoquant INT) mais on va ici voir comment partir en interruption lors qu'on appui sur le bouton. Si le bouton est pressé, alors INTR = 1. La détection d'une interruption se fait avant l'exécution de chaque instruction du programme principal (à l'adresse 0x00 de la ROM) avec des signaux CodeMCount particulier. Je vous rappelle la sémantique de CodeMCount qui permet de piloter le multiplexeur du MicroPC:

  • Si CodeMCount=0b000, alors SMux=MicroPC+1
  • Si CodeMCount=0b001, alors SMux=@Adr
  • Si CodeMCount=0b010, alors SMux=InstCode
  • Si CodeMCount=0b011 et Z=0, alors SMux=MicroPC+1
  • Si CodeMCount=0b011 et Z=1, alors SMux=@Adr

On y ajoute les codes suivants :

  • Si CodeMCount=0b100 et INTR & IF=0, alors SMux=@Adr
  • Si CodeMCount=0b100 et INTR & IF=1, alors SMux=MicroPC+1

Lorsqu'il n'y a pas d'interruption, le MicroPC se branche directement sur l'adresse du Fetch/Decode (0x08), il n'y a donc pas de surcoût à détecter si une interruption est levée ou non. Si il y a une interruption, la prochaine instruction en ROM[0x01] branchera vers les micro-instructions de l'instruction INT (0xe0).

Si une interruption est détectée, il faut gérer l'interruption. La gestion de l'interruption se fait par les micro-instructions à l'adresse 0xe0 (INT). On va supposer ici que notre interruption n'est pas masquable, non interruptible. Aussi, je vous rappelle qu'une interruption doit être gérée de manière transparente pour le programme qui est entrain de tourner, c'est à dire qu'il faut sauvegarder les registres avant de partir vers le programme de l'interruption, qu'il faudra recharger après la routine d'interruption (RTI). Enfin, le programme exécuté lors de l'interruption sera ici par convention à l'adresse 0x0002 en RAM. Le microcode pour INT devra donc :

  • mettre à zéro le registre IF, accuser réception de l'interruption (signal de contrôle INTA)
  • sauvegarder les registres (A, B, PC) sur la pile
  • charger le PC avec l'adresse 0x0002. Notez que vous disposez du signal de contrôle INTA pour libérer sur le bus A la valeur 0x0002 qui correspond à l'adresse du vecteur d'interruption.

On dira alors qu'on est parti en interruption. Les vecteurs d'interruptions seront codés en RAM avec des instructions JMP. En assembleur, le début de votre RAM devra donc ressembler à:

0x0000    JMP init
0x0002    JMP handler

avec init l'adresse de votre programme principal et handler l'adresse du programme à exécuter lorsque l'interruption est levée (notez que "JMP handler" doit ici être à l'adresse 0x0002).

Le programme associé à l'interruption s'exécute alors (par exemple, il modifie la valeur d'une variable en RAM). A la fin du programme d'interruption, il faut revenir au programme interrompu en invoquant l'instruction RTI (ReTurn from Interrupt). Il faut donc remettre le chemin de données dans l'état dans lequel il était avant le départ en interruption, c'est à dire:

  • récupérer les registres de la pile
  • réactiver les interruptions en mettant à un le registre IF

Les étapes de départ en interruption, exécution du programme d'interruption et de retour d'interruption sont illustrées ci-dessous.

Schéma de principe de fonctionnement des interruptions

Question

Travail à réaliser

Commencez par modifier la ROM à l'adresse 0x00 pour ajouter les micro-instructions permettant de sauter à l'adresse INT (0xe0) si une interruption est levée et de sauter à l'adresse des micro-instructions de Fetch/Decode (0x08) sinon,

Définissez le microcode pour les instructions STI (0xd4) , CLI (0xd0),

Définissez le microcode pour les instructions INT (0xe0) et RTI (0xe8).

Testez votre architecture avec le programme suivant irq_bouton_simple.asm, irq_bouton_simple.mem. Le programme principal incrémente un compteur, l'interruption alterne 0, 1 sur le deuxième afficheur

Pour faciliter votre travail de calcul des micro-instructions, je vous propose d'utiliser

Le signal de contrôle EOI ne sera utilisé que dans la seconde partie sur les interruptions multiples.

Vous pouvez passer alors à un problème un peu plus compliqué.

Question

Je vous propose aussi le programme irq_bouton.asm, irq_bouton.mem qui incrémente un compteur réinitialisé (en principe) chaque fois qu'on appui sur le bouton.

En pratique, si vous testez bien, ça ne marche pas tout le temps et le compteur n'est pas toujours réinitialisé comme on pense qu'il devrait l'être.

Savez-vous pourquoi ? Savez vous modifier le programme pour garantir que le compteur soit correctement réinitialisé ?

Une application des interruptions : un contrôleur clavier

Je vous propose une application des interruptions en ajoutant un clavier et un écran à notre architecture. On aimerait que les caractères saisis sur le clavier (le clavier s'utilise en tapant des caractères tandis que le clavier est sélectionné avec le poke tool) soient affichés à l'écran. Dans votre architecture archi_irq.circ :

  • le clavier dispose d'un registre de contrôle accessible en écriture sur le port 0x6000 et un registre de données en lecture accessible à l'adresse 0x1003,
  • l'écran est accessible en écriture à l'adresse 0x1004.

Danger

Vous devez opérer une mise à jour de l'architecture pour que ce soit le clavier qui lève l'interruption plutôt que le bouton que vous utilisiez précédemment. Le clavier fournit le signal INTKbd qu'il vous suffit d'envoyer sur l'étiquette INTR à la place de la sortie de la bascule attachée au bouton.

Architecture avec un périphérique clavier géré par interruption

Spécifions les interfaces. L'écran est un périphérique mappé en mémoire, chaque fois qu'on écrit un caractère à l'adresse 0x1004, ce caractère est écrit à la suite des caractères précédents.

Le clavier, quand à lui, est un peu plus compliqué. Il dispose d'un contrôleur, ici un registre seul. Ce registre est mappé en mémoire et est accessible en écriture à l'adresse 0x6000. Dans la vraie vie, le contrôleur clavier accepte plein de commandes, comme par exemple allumer le LED "Num lock", "Caps Lock", etc.. (voir par exemple https://wiki.osdev.org/PS/2_Keyboard). Ici, notre contrôleur est très simple, il ne dispose que d'un bit. Ce bit sert à :

  • faire basculer le caractère du clavier vers le registre de sortie,
  • vider le buffer clavier.

Pour déclencher cette action, il suffit d'écrire "0x0000" puis "0x0001" sur le contrôleur. Une fois que le contrôleur a transféré le caractère dans le registre de sortie, on y accède en lecture sur le port 0x6000.

Info

pourquoi 0x6000 ? En fait, c'est un petit clin d'oeil au contrôleur clavier intel dont les ports sont 0x60 et 0x64 sur le bus d'adresse d'entrées/sorties.

Question

Travail à réaliser

Ecrivez la RAM telle que le programme principal exécute un programme, par exemple incrémente un compteur en affichant le résultat sur un des afficheurs 7 ségments, et l'interruption affiche les caractères saisis au clavier sur l'écran.

Gestion de plusieurs interruptions

L'architecture précédente ne sait gérer qu'une seule interruption. On se propose d'aller plus loin. Notre architecture est étendue d'un composant qui s'inspire de l'intel 8259A, le Programmable Interrupt Controller.

Composant inspiré du PIC pour gérer plusieurs interruptions

Comme vu en cours, ce composant offre la possibilité de prendre en charge \(8\) interruptions. Ces interruptions sont hiérarchiques (IR0 est la plus prioritaire et IR7 est la moins prioritaire) et masquables (d'un point de vue matériel et logiciel).

On va se donner une histoire prétexte à implémenter plusieurs interruptions hiérarchiques. On suppose qu'il existe un calcul principal, par exemple le contrôle d'une chaîne de production robotique. On voudrait sécuriser ce système en ajoutant un bouton d'arrêt d'urgence, qu'on appelle STOP. On imagine alors qu'une vérification du système est réalisée et qu'il est possible de relancer la chaîne de production en appuyant sur un bouton GO si jamais il n'y a pas de problème.

Mise à jour des micro-instructions

Le composant PIC (Programmable Interrupt Controller) qui gère les multiples instructions disposent d'un état interne dont la logique est un peu plus compliqué que lorsqu'on nous n'avions qu'une interruption à gérer. En particulier, il est désormai nécessaire d'instruction un nouveau signal de contrôle EOI qui permet au CPU d'indiquer au PIC lorsqu'il a terminé d'exécuter la routine associée à l'interruption en cours de gestion.

Par ailleurs, concernant le masque matériel des interruptions. Lorsque nous avions une seule interruption, nous avons utilisé le flag IFlag. Mais ce flag masque complètement toutes les interruptions, ce qui fonctionnait lorsque nous ne disposions que d'une interruption mais ne fonctionne plus lorsque nous en avons plusieurs. Le masque matériel, qui implémente la hierarchie des interruptions, est maintenant pris en charge par le PIC.

Question

Apportez les modifications des micro-instructions INT et RTI pour :

  • ne plus modifier le registre IFlag lors du départ et retour d'interruption
  • émettre le signal EOI au retour d'interruption

On peut maintenant utiliser cette nouvelle architecture pour résoudre notre problème.

Arrêt d'urgence avec des interruptions hiérarchiques

Téléchargez l'architecture archi_pic_irq.circ. Sur cette architecture, les modifications suivantes sont apportées :

  • un bouton GO émet une interruption sur la ligne IR1
  • un bouton STOP émet une interruption sur la ligne IR2
  • une LED permet d'affiche le statut (en exécution ou à l'arrêt). Elle est addressable en mémoire à l'adresse 0x1003 et utilise le bit de poids faible pour changer de couleur

Question

Vous devez écrire le contenu de la RAM pour résoudre notre problème. Je vous suggère :

  • une boucle principale qui incrémente un compteur comme on l'a déjà vu à plusieurs reprises
  • une routine d'interruption pour le bouton go
  • une routine d'interruption pour le bouton stop
  • éventuellement une routine d'interruption pour le clavier

Pour l'arrêt d'urgence, je vous suggère d'utiliser une boucle tant que qui teste la valeur d'une valeur dont le contenu est initialisé par l'arrêt d'urgence et modifié par la routine go.

Pour la mise en oeuvre, je vous conseille fortement d'écrire votre programme en utilisant les instructions symboliques avant de faire la traduction en binaire.

La structure globale du programme peut ressembler à :

JMP init
JMP go
JMP stop
JMP kbd
init: LDSPi ...  ; initialisation
...
loop: ... ; boucle pour le compteur
...
stop: ... ; routine pour le bouton STOP
...
loopstop: ... ; attente active
...
go: ... ; routine pour repartir
....
kbd: ... ; routine pour le clavier
...

Cela commence à devenir fastidieux d'écrire un programme. Rassurez-vous, dans le prochain TP, nous allons introduire un assembleur, qui est un programme qui réalise la traduction de code assembleur à code binaire automatiquement.

Ressources