Un dernier pour la route ! Dans la foulée des précédentes séries d'articles prenant pour prétexte la programmation d'une cracktro (1 et 2) et celle d'un sine scroll (1, 2, 3, 4 et 5), pour présenter dans le détail la manière d'attaquer le hardware de l'Amiga 500 en assembleur 68000 après avoir court-circuité l'OS, il convient de revenir sur un point délicat qui a été soulevé, à savoir celui de la gestion des "interruptions hardware".
Le problème plus particulièrement étudié est le suivant : comment mettre sous "interruption VERTB" un bout de code dont l'unique fonction est de modifier la couleur de fond (COLOR00) ? Pour bien en visualiser les effets, nous allons élaborer un scénario au fil duquel la couleur de fond sera notamment modifiée par un tel bout de code quand le faisceau d'électrons atteindra certaines positions verticales :
Cliquez ici pour télécharger le source du programme présenté ici.
Mise à jour du 02/10/2018 : Tous les sources ont été modifiés pour intégrer une section "StingRay's stuff" qui permet d'assurer le bon fonctionnement sur tous les modèles d'Amiga, notamment dotés d'une carte graphique.
Le fonctionnement des interruptions hardware
Comme cela a été expliqué dans un précédent article, le hardware de l'Amiga génère des événements qui peuvent entraîner des interruptions de niveau 1 à 6 du CPU.
Par exemple, l'événement VERTB, qui survient quand le faisceau d'électrons ayant atteint le bas de l'écran entame son retour vers le haut de ce dernier pour afficher la trame suivante, peut entraîner une interruption de niveau 3 du CPU. Le CPU cesse alors d'exécuter le programme principal pour exécuter le gestionnaire d'interruption dont l'adresse est stockée dans une table des vecteurs d'interruptions. S'agissant d'une interruption de niveau 3, le M68000 8-/16-/32-Bit Microprocessors User's Manual nous apprend qu'il s'agit du vecteur 27, qui renvoie au gestionnaire d'interruption logé à l'adresse $6C :
C'est donc par abus de langage qu'on parle d'interruption hardware, comme par exemple d'interruption VERTB pour désigner l'interruption dont il vient d'être question. Toutefois, cette convention étant pratique, nous l'utiliserons par la suite.
La liste des événements signalés le hardware qui sont susceptibles d'entraîner une interruption de niveau 1 à 6 du CPU figure dans la documentation du registre INTENA dans l'Amiga Hardware Reference Manual :
Bit | Nom | Niveau | Description |
15 | SET/CLR | Ce bit est décrit plus loin. | |
14 | INTEN | Ce bit est décrit plus loin. | |
13 | EXTER | 6 | Interruption externe |
12 | DSKSYN | 5 | Le contenu du registre DSKSYNC correspond aux données du disque |
11 | RBF | 5 | Buffer de réception du port série plein |
10 | AUD3 | 4 | Bloc du canal audio 3 terminé |
09 | AUD2 | 4 | Bloc du canal audio 2 terminé |
08 | AUD1 | 4 | Bloc du canal audio 1 terminé |
07 | AUD0 | 4 | Bloc du canal audio 0 terminé |
06 | BLIT | 3 | Opération au Blitter terminée |
05 | VERTB | 3 | Début du blanc vertical |
04 | COPER | 3 | Copper |
03 | PORTS | 2 | ports d'entrée/sortie et timers |
02 | SOFT | 1 | Réservé pour une interruption initiée par logiciel |
01 | DSKBLK | 1 | Bloc disque terminé |
00 | TBE | 1 | Buffer de transmision du port série vide |
Le hardware signale toujours les événements énumérés en positionnant les bits correspondants dans un autre registre, INTREQ. Par contre, il n'interrompt pas toujours le CPU dans la foulée. En effet, tout dépend de la gestion que le registre INTENA permet de configurer.
Lorsqu'une valeur 16 bits X est écrite dans INTENA, l'état du bit SET/CLR dans X indique si un bit d'INTENA désigné par un bit positionné dans X doit être positionné ou effacé. Par exemple, pour inhiber le déclenchement d'une interruption niveau 3 du CPU sur un événement VERTB, nous devons écrire $0020 dans INTENA. A l'inverse, pour l'activer, nous devons écrire $8020 dans ce registre. Toutefois, il faut encore compter avec le bit INTEN.
Le bit INTEN est une sorte d'interrupteur général :
- Lorsque INTEN est effacé, le déclenchement de toute interruption CPU sur événement hardware est inhibé, quand bien même le bit correspondant à un événement est positionné dans INTENA. Par exemple, si jamais INTEN n'était pas positionné, écrire $8020 dans INTENA ne suffit pas à activer le déclenchement d'une interruption de niveau 3 du CPU sur événement VERTB. Il faudrait plutôt écrire $C020 dans le registre pour positionner simultanément INTEN et VERTB, ou alors y écrire $8020 pour positionner VERTB puis $C000 pour positionner INTEN - l'inverse étant possible.
- Lorsque INTEN est positionné, le déclenchement d'interruptions CPU sur événements hardware est activé, mais uniquement pour les événements dont les bits correspondants sont positionnés dans INTENA. Par exemple, écrire $C000 n'a d'autre effet que d'activer le déclenchement d'une interruption de niveau 3 du CPU sur événement VERTB, si jamais VERTB est le seul bit correspondant à un événement à avoir été positionné dans INTENA.
Une conséquence importante de ce fonctionnement, c'est que nous pouvons écrire dans INTENA pour modifier la manière dont le hardware génère des interruptions CPU sur événements hardware - la gestion des interruptions hardware pour faire plus court - de manière très sélective. Dans l'exemple suivant, la première instruction ne fait qu'activer l'interruption hardware TBE, et la seconde ne fait qu'inhiber l'interruption VERTB - tout ce qui concerne la gestion des autres interruptions hardware et l'interrupteur générale INTEN est inchangé :
move.w #$8001,INTENA(a5) move.w #$0020,INTENA(a5)
Pour cette raison, seule une lecture de INTENAR, le registre homologue de INTENA en lecture seule - INTENA est en écriture seule -, permet d'être exactement renseigné sur la gestion des interruptions hardware à un instant donné.
Couper les interruptions hardware
Nous n'allons pas rentrer dans le détail des opérations requises pour mettre en place un environnement de développement en utilisant WinUAE pour Windows et pour écrire un programme minimal en assembleur 68000 qui prend le contrôle total du hardware de l'Amiga. Tout cela a déjà été soigneusement exposé dans cet article.
D'emblée, concentrons-nous donc sur ce que concerne la gestion des interruptions hardware. Après avoir court-circuité l'OS, il s'agit de prendre le contrôle des six vecteurs d'interruption du CPU qui, parmi les 255 dont ce dernier dispose, correspondent aux interruptions de niveau 1 à 6. Autrement dit, les vecteurs 25 à 30.
Tout d'abord, nous devons sauvegarder l'état de la gestion des interruptions hardware pour la restaurer à la fin. Pour cela nous lisons et stockons dans des variables intena et intreq le contenu des registres INTENAR et INTREQR, versions en lecture seule des registres INTENA et INTREQ :
move.w INTENAR(a5),intena move.w INTREQR(a5),intreq
Nous pouvons alors couper toutes les interruptions hardware :
move.w #$7FFF,INTENA(a5)
Dans la foulée, nous acquittons tous les événements que le hardware pouvait avoir signalé. Certes, le hardware va continuer à en signaler, mais du moins saurons-nous à partir de cet instant que si le hardware positionne un bit dans INTREQ, ce sera pour signaler un événement survenu après que nous ayons pris la main :
move.w #$7FFF,INTREQ(a5)
Enfin, nous détournons les vecteurs des interruptions de niveau 1 à 6 en les renvoyant sur un sous-programme de type gestionnaire d'interruption qui nous appartient. Ces vecteurs pointent sur les adresses $64 à $78 :
lea $64,a0 lea vectors,a1 REPT 6 move.l (a0),(a1)+ move.l #_rte,(a0)+ ENDR
Plus loin, après le RTS final du programme principal, nous codons le gestionnaire d'interruption _rte en question :
_rte: rte
Ce gestionnaire ne fait donc strictement rien, pas même acquitter la notification de l'événement hardware qui est à l'origine de son appel - nous reviendrons sur ce point.
Un scénario à base d'interruptions VERTB
Comme expliqué, INTREQ fonctionne exactement comme INTENA, sauf qu'il sert à signaler des événements plutôt qu'à activer/inhiber le déclenchement d'interruptions CPU associées aux événements en question.
Il faut rajouter deux choses sur INTREQ :
- rien ne peut empêcher le hardware de continuer à signaler dans INTREQ les événements qui surviennent ;
- il est possible d'écrire au CPU dans INTREQ pour simuler un ou plusieurs événements hardware.
Nous allons illustrer ces points. Le scénario retenu est le suivant :
- le programme principal attend le quart supérieur de l'écran (ligne DISPLAY_Y+(DISPLAY_DY>>2)) pour déclencher une interruption VERTB dont le gestionnaire passe COLOR00 à $00F0 (vert) ;
- la Copper list attend la moitié de l'écran (ligne DISPLAY_Y+(DISPLAY_DY>>1)) pour passer COLOR00 à $0F00 (rouge) ;
- le hardware attend le bas de l'écran (fin de la ligne 312 en PAL) pour déclencher une interruption VERTB dont le gestionnaire passe COLOR00 à $000F (bleu).
Coder le scénario
Commençons la Copper list.
Cette dernière se résume à une instruction WAIT pour attendre le faisceau d'électrons atteigne ou dépasse la mi-hauteur de l'écran, suivie d'une instruction MOVE pour stocker $0F00 (rouge) dans COLOR00 :
move.l copperlist,a0 move.w #((DISPLAY_Y+(DISPLAY_DY>>1))<<8)!$0001,(a0)+ move.w #$FF00,(a0)+ move.w #COLOR00,(a0)+ move.w #$0F00,(a0)+
Bien évidemment, nous n'oublions pas de terminer la Copper list par le traditionnel WAIT impossible :
move.l #$FFFFFFFE,(a0)+
Codons maintenant le programme principal.
Tant que l'utilisateur n'a pas pressé le bouton gauche de la souris, ce programme attend en boucle le faisceau d'électrons au premier quart de hauteur de l'écran. Il stocke alors $00F0 (vert) dans une variable color dans laquelle le gestionnaire d'interruption VERTB trouvera la couleur à stocker dans COLOR00. Enfin, il provoque l'appel à ce gestionnaire en générant une interruption VERTB par une écriture dans INTREQ :
_loop: ;Attendre le quart supérieur de la hauteur de l'écran _wait0: move.l VPOSR(a5),d0 lsr.l #8,d0 and.w #$01FF,d0 cmp.w #DISPLAY_Y+(DISPLAY_DY>>2),d0 blt _wait0 ;Déclencher une interruption VERTB move.w #$00F0,color move.w #$8020,INTREQ(a5) ;Tester la pression du bouton gauche de la souris btst #6,$bfe001 bne _loop
Attention ! Ce code doit être complété par une boucle _wait1 venant immédiatement après la boucle _wait0. Son objet est d'attendre le faisceau d'électrons à la ligne 0 :
_wait1: move.l VPOSR(a5),d0 lsr.l #8,d0 and.w #$01FF,d0 bne _wait1
En effet, l'exécution du code de notre programme prend moins de temps qu'il n'en faut au faisceau d'électrons pour terminer de balayer la ligne DISPLAY_Y+(DISPLAY_DY>>2). A défaut d'attendre une ligne suivante - en l'occurrence, la ligne 0, mais cela aurait parfaitement pu être la ligne du dessous DISPLAY_Y+(DISPLAY_DY>>2)+1 -, le code du programme ne serait donc pas exécuté une fois par trame, mais plusieurs fois, ne produisant alors pas l'effet désiré.
Enfin, il nous reste à coder le gestionnaire d'interruption.
Avant toute chose, il faut se rappeler qu'un gestionnaire d'interruption est un sous-programme exécuté alors qu'un programme était en cours d'exécution. Partant, il faut au moins sauvegarder le contenu des registres qui seront utilisés par le gestionnaire pour être en mesure de les restaurer quand ce dernier se terminera. Cela s'effectue généralement à l'aide de l'instruction MOVEM pour stocker et lire le contenu de registres dans la pile.
Ensuite, il faut savoir que tant que l'événement n'est pas acquitté en effaçant son bit dans INTREQ, le hardware génère l'interruption CPU associée. Pour s'en convaincre, il suffit d'oublier d'acquitter l'événement VERTB dans le gestionnaire d'interruption et de modifier le programme principal pour lui demander de passer en boucle COLOR00 à $0FF0 (jaune) :
_loop: move.w #$0FF0,COLOR00(a5) btst #6,$bfe001 bne _loop
L'écran est totalement bleu ou presque, n'étant ponctué de jaune que lors des rares cycles d'exécution que la fin du gestionnaire d'interruption permet au CPU de récupérer pour continuer à exécuter le programme, avant qu'il ne doive exécuter de nouveau le gestionnaire d'interruption :
Dans ces conditions, le gestionnaire d'interruption doit impérativement acquitter l'événement à la suite duquel il a été appelé.
Dès lors, le code de notre gestionnaire d'interruption doit au moins être le suivant :
_vertb: ; movem.l d0-d7/a0-a6,-(sp) ;Indispensable, mais à limiter aux registres modifiés move.w #$0020,INTREQR(a5) ; movem.l (sp)+,d0-d7/a0-a6 ;Indispensable, mais à limiter aux registres modifiés rte
En l'espèce, son code complet est le suivant :
_VERTB: ; movem.l d0-d7/a0-a6,-(sp) ;Indispensable, mais à limiter aux registres modifiés move.w color,COLOR00(a5) move.w #$000F,color move.w #$0020,INTREQ(a5) ; movem.l (sp)+,d0-d7/a0-a6 ;Indispensable, mais à limiter aux registres modifiés rte
La Copper list, le programme principal et le gestionnaire d'interruption étant codés, il ne reste plus qu'à activer l'ensemble. Après avoir activé le Copper et lui avoir demandé d'exécuter en boucle la Copper list, il suffit de stocker l'adresse de notre gestionnaire d'interruption à l'adresse contenue dans le vecteur de l'interruption de niveau 3 du CPU, c'est-à-dire $6C... :
move.l #_VERTB,$6C
...avant d'autoriser de nouveau le hardware à générer cette interruption de niveau 3 quand il détecte l'événement VERTB :
move.w #$C020,INTENA(a5) ;INTEN=1, VERTB=1
Le résultat produit est celui présenté au début de cet article.
Restaurer les interruptions hardware
Lorsque l'utilisateur clique sur le bouton gauche de la souris, le programme se termine en rendant aussi proprement que possible la main à l'OS. Sans revenir sur les autres tâches qui nous incombent - elles sont détaillées dans cet article -, concentrons-nous donc encore sur ce que concerne la gestion des interruptions hardware.
Nous commençons donc pas couper de nouveau la génération d'interruptions CPU par le hardware... :
move.w #$7FFF,INTENA(a5) move.w #$7FFF,INTREQ(a5)
...avant de restaurer les vecteurs d'interruption... :
lea $64,a0 lea vectors,a1 REPT 6 move.l (a1)+,(a0)+ ENDR
...et de rétablir la génération des interruptions CPU par le hardware :
move.w intreq,d0 bset #15,d0 move.w d0,INTREQ(a5) move.w intena,d0 bset #15,d0 move.w d0,INTENA(a5)
C'est fini !
Une subtilité pour conclure...
Noter que le Copper a accès en écriture à INTREQ. Par conséquent, il est parfaitement possible de déclencher une interruption VERTB à n'importe quelle position du faisceau d'électrons que le Copper peut repérer - comme cela a été expliqué dans cet article, ses capacités en la matière sont limitées, la granularité verticale étant certes d'une ligne, mais la granularité horizontale n'étant que de 4 pixels en basse résolution.
Il suffit donc d'utiliser un MOVE dans la Copper list :
move.l copperlist,a0 ;... move.w #INTREQ,(a0)+ move.w #$8020,(a0)+
A titre, d'exemple, voici le résultat produit par une variante du programme présenté plus tôt. Ici, le Copper déclenche une interruption VERTB quand le faisceau d'électrons atteint ou dépasse le dernier quart de la hauteur de l'écran, avec pour effet de passer COLOR00 à $0000 (noir) :
Cliquez ici pour télécharger le source du programme qui produit cet effet.