— Cet article serait à enrichir avec de nouveaux sujets ; nous n'avions que peu traité du système dans Quasar CPC. Avis aux amateurs.
Nous allons ici traiter de la programmation utilisant le système en assembleur. Il s'agit de vous présenter divers packs de routines offertes par le firmware du CPC qui peuvent être fort utiles lors de la programmation de logiciels ou de petits jeux. En effet, le système du CPC n'est pas si mal fait et il est parfois judicieux de l'utiliser plutôt que de réinventer la roue.
— Basé sur l'article publié dans Quasar CPC numéro 9, Initiation à l'Assembleur, par SNN.
Vous nous l'avez demandé, alors, comme nous ne vivons que pour vous servir, vous l'avez : les RSX. En fin de compte, c'est tellement simple à faire que je me demande pourquoi vous désirez plus d'informations. Bref, allons-y.
Les RSX sont une merveille, c'est ce qui fait que le BASIC d'Amstrad est mille fois supérieur à nombre de BASIC qui ne peuvent pas être étendus de la sorte.
Mais si les RSX sont déjà très intéressantes telles quelles, elles le deviennent encore plus lorsqu'on les exploite pour des programmes en ROM ! En effet, dès l'allumage de votre ordinateur vous avez tout un tas de commandes BASIC supplémentaires qui ne prennent pas de place en RAM puisqu'elles sont en ROM ; tout au plus quelques octets seront utilisés pour des variables d'environnement !
Une RSX, c'est donc une eXtension Résidente du Système. Elle sert à augmenter le nombre de commandes propres au BASIC… et ne se programme qu'en assembleur (vous voyez le paradoxe ?). En deux mots : vous programmez en assembleur ? Bravo ! Vous allez pouvoir faire des merveilles sous BASIC. Chouettos !
En fait, les RSX sont une merveille, c'est ce qui fait que le BASIC d'Amstrad est mille fois supérieur à nombre de BASIC qui ne peuvent pas être étendus de la sorte. Il existe deux types de RSX : celles installées en RAM et celles initialisées depuis des ROMs d'extensions. Ces commandes vous sont normalement bien familières, elles sont précédées par un symbole de convention : “|” sur qwerty (pipe, à prononcer “pa-i-pe”) qui s'écrit “ù” sur azerty… |REN
, |ERA
, etc. sont donc des RSX (installées par la ROM CP/M en l'occurrence).
Nous allons ici plus particulièrement nous intéresser aux RSX en RAM quoique le mécanisme de passage des paramètres reste le même pour les RSX en ROM2); il faut savoir que ce type de RSX n'est ni plus ni moins qu'un CALL adr,n paramètres
. À noter enfin que les RSX installées en RAM sont prioritaires par rapport à celles disponibles en ROM ce qui permet la surcharge de celles-ci.
Pour que le miracle s'opère, monsieur firmware a en lui un organe épatant qui gère la création de telles RSX. C'est le petit &BCD1
.
Voici son paramètrage :
BC
pointe sur ce qu'on appelle le “vecteur” de la RSX. Ce vecteur est une base de données contenant :
STR
de Maxam fait ça tout seul pour vous),JP
pointant sur le code de la RSX proprement dit.LD BC,VECTEUR ... VECTEUR STR "NOM" JP &8000
Vient ensuite HL. Ne vous posez pas de questions sur son rôle : il pointe sur un buffer de 4 octets que se réserve le système pour travailler. On est heureux pour lui.
LD HL,BUFFER ... BUFFER DS 4
Et voilà ! On lance le &BCD1
. La RSX est initialisée.
Nous allons voir à présent un petit programme d'exemple qui vous sera super utile en BASIC, c'est mon |LDIR
(ou ùLDIR
en azerty). Il sert à transférer BC octets de HL vers DE, comme en assembleur.
ORG &8000 LD A,&C9 ; Pour ne pas exécuter deux fois ! LD (&8000),A LD BC,VECTEUR LD HL,BUFFER JP &BCD1 ; Équivalent de CALL suivi de RET VECTEUR DW TABLE JP LDIRPRG BUFFER DS 4 TABLE STR "LDIR" LDIRPRG CP 3 ; Pour vérifier qu'on a trois paramètres RET NZ LD C,(IX+4) LD B,(IX+5) LD L,(IX+2) LD H,(IX+3) LDIR ; DE contient déjà le dernier paramètre d'entrée RET
Syntaxe : |LDIR,longueur,source,destination
Quelques renseignements sur la mise en forme des paramètres. Ceux-ci sont tous stockés dans une pile sur laquelle pointe IX. Le dernier paramètre sera stocké aux niveaux d'indexage +1 et +0. L'avant dernier, aux niveaux +2 et +3 (poids fort en +3 et poids faible en +2). L'antépénultième (wach !) aux niveaux +4 et +5, etc..
Il faut savoir que le dernier paramètre sera aussi stocké dans DE. L'accu contient le nombre de paramètres d'entrée fournis (d'où le CP 3
). En fait, le passage des paramètres est rigoureusement le même que dans le cas d'un CALL
.
— Basé sur les articles publiés dans Quasar CPC numéro 15 et numéro 16, Assembleur : Software, par Zik.
Dans cette section, je vais tenter de vous expliquer la gestion des interruptions par le noyau (kernel) du firmware du CPC.
Je vous préviens tout de suite, certaines subtilités m'échappent encore et si vous avez des compléments à m'apporter (notamment sur les vecteurs &BCFE
et &BD01
), je vous serais reconnaissant de m'en faire part. J'espère que la crédibilité de cet article n'en souffrira pas trop !
La technique des interruptions permet de réagir rapidement à des événements tout en n'interrompant le programme que lorsqu'il y a une action à réaliser. Sans cela votre programme serait obligé de contrôler régulièrement ce qui se passe pour voir s'il doit réagir ou non ; cette technique s'appelle du polling.
Sur CPC, on observe un mélange de ces deux techniques. Matériellement, le Gate Array provoque une interruption du Z80 tous les 300èmes de seconde, c'est-à-dire 6 fois pendant un balayage écran. Une de ces interruptions est synchronisée sur la VBL du CRTC. C'est de ce timer (le “fast ticker”) que tout découle.
À partir du “fast ticker” sont générées logiciellement :
Donc, vous pouvez demander au système d'exploitation de lancer vos routines en définissant la fréquence d'exécution et si elle doit avoir lieu au moment du “frame flyback”3), “ticker”4) ou “fast ticker”5). Cette demande se fait par l'intermédiaire d'un “event block” (bloc d'événement). Chaque routine dispose donc de son “event block”, qui donne toutes les informations qui la concerne (l'emplacement, la fréquence d'appel, la priorité, etc.).
Certains octets du bloc sont réservés au système d'exploitation qui y place des pointeurs qui lui permettent de passer d'un bloc à l'autre. Un bloc est forcément placé dans les 32ko centraux de la mémoire, car c'est la seule zone directement accessible indépendamment des ROMs connectées. Méfiez-vous cependant des banks.
Quand la routine correspondant à un bloc doit être exécutée, le bloc est rangé dans la “pending queue” (file d'attente). Ceci est appelé “kicking”.
Voici la structure de la partie commune à toute sorte d'événement (appelé bloc d'événement) :
%0000
est la priorité la plus faible). Uniquement utilisé pour les événements synchrones.Le bloc d'événement est précédé d'un bloc de contrôle qui comporte 2 octets pour un “frame fly” ou un “fast ticker”, ce sont deux octets de chaînage (à ne pas modifier !) dans la “frame fly list” ou la “fast ticker list”. Pour un “ticker” il compte 6 octets :
L'octet ROM select pour une “far address” se compose comme suit :
Voici enfin la liste de tous les vecteurs nécessaires pour manipuler ces events.
BCD7
: KL New Frame Fly
Créer et ajouter un bloc d'événement à la liste de ceux à exécuter au moment du “frame flyback”.
BCDA
: KL Add Frame Fly
Ajouter un bloc d’événement dans la liste.
BCDD
: KL Del Frame Fly
Supprimer un bloc de la “frame fly list”.
BCE0
: KL New Fast Ticker
Créer et ajouter un bloc d’événement à la liste de ceux à exécuter tous les 300èmes de seconde.
BCE3
: KL Add Fast Ticker
Ajouter un bloc d'événements dans la liste.
BCE6
: KL Del Fast Ticker
Supprimer un bloc de la “fast ticker list”.
BCE9
: KL Add Ticker
Ajouter un bloc ticker et créer un bloc de contrôle. Il faut d'abord faire une initialisation avec &BCEF
.
BCEC
: KL Del Ticker
Supprimer un bloc ticker de la “ticker list”.
BCEF
: KL Init Event
Créer un bloc d'événement pour un “ticker” (exécution à une période multiple du 50ème de seconde).
BCF2
: KL Event
“Kick” un bloc d'événement.
BCF5
: KL Sync Reset
Supprimer la “synchronous pending queue”. C'est-à-dire faire le ménage dans toutes les files d'attente des événements temporisés.
BCF8
: KL Del Synchronous
Supprimer un bloc déterminé de la file d'attente (la “pending queue”).
BCFB
: KL Next Sync
Rechercher l'événement suivant à traiter. Permet de savoir s'il reste des événements à exécuter.
BCFE
: KL Do Sync
Exécuter une routine d'événement.
BD01
: KL Done Sync
Terminer le traitement d'un événement.
BD04
: KL Event Disable
Verrouiller des événements normalement simultanés. Les événements urgents simultanés ne sont pas verrouillés.
BD07
: KL Event Enable
Autoriser les événements simultanés normaux.
BD0A
: KL Disarm Event
Verrouiller un bloc d'événement (compteur négatif).
B921
: KL Poll Synchronous
Y'a-t-il un événement synchrone à traiter ?
BCC8
: KL Choke Off
Réinitialise le kernel (files d'interruption, RSX, etc.).
Nous avons vu qu'il existe trois familles d'événements qui diffèrent par leur fréquence d'appel. Ils sont nommés “fast ticker”, “ticker” et “frame flyback”. Pour les déclarer vous devez créer un bloc, lequel bloc est séparé en deux parties : le bloc de contrôle puis le bloc d'événement (dans cet ordre en se déplaçant vers les adresses croissantes). Le bloc d'événement est le même quel que soit le type d'événement ; par contre le bloc de contrôle compte 6 octets pour un “ticker” et deux seulement pour les autres.
Méfiez-vous des conditions d'appel des vecteurs, certains nécessitent l'adresse du bloc de contrôle et d'autres celle du bloc d'événement.
Oui, je vous ai parlé plus haut de compteur 16 bits et 8 bits sans vraiment vous dire leur utilité et leur fonctionnement, voici donc.
Le compteur de l'octet 2 du bloc d'événement est sur 8 bits. S'il contient un valeur positive (de &00
à &7F
) il indique le nombre d'interruptions “ratées” et qui seront rattrapées à la prochaine occasion. Alors, la routine “en retard” sera exécutée à chaque traitement des routines d'interruption (donc pas à la fréquence normale) afin de se remettre à jour. Ce phénomène est illustré par le programme BASIC (Prog 1) avec en plus une démonstration de la gestion des priorités. J'y reviendrai plus bas.
Le plus important est de remarquer que, grâce à ce compteur, le système s'arrange pour que globalement la routine que vous lui avez confiée soit exécutée à la fréquence demandée. Remarquez aussi que le problème de rattrapage d'interruptions ne se pose pas pour les événements asynchrones puisqu'ils sont forcément traités à chaque interruption.
Si le compteur 8 bits est négatif, alors la routine d'interruption n'est plus exécutée. C'est ce que fait le vecteur &BD0A
en mettant la valeur &C0
. On a donc la possibilité d'empêcher l'exécution d'un événement déterminé sans retirer son bloc de la “pending queue”. Pour que l'exécution de la routine reprenne il faut placer la valeur 0 à l'adresse du compteur (aucun vecteur ne s'occupe de cela à ma connaissance et il faut donc le faire à la main). La routine recommence alors à être appelée à sa fréquence normale. Ceci marche pour tout type d'événement, y compris les asynchrones.
Puisque nous sommes dans les compteurs, parlons de ceux du bloc de contrôle des “tickers”. Lors de la déclaration d'un “ticker” vous devez spécifier deux paramètres 16 bits : le “tick count” et le “reload count”. Le premier définit la valeur initiale du compteur (en 50èmes de seconde) qui est décrémenté à chaque “ticker” et mis à la valeur du “reload count” quand il arrive à 0. Il est “kické” à cet instant. Voici ce qui se passe dans deux cas “particuliers” :
|
Parlons maintenant des vecteurs &BD04
et &BD07
. Ils correspondent respectivement aux commandes DI
et EI
du BASIC. Dans leurs descriptifs (voir la section précédente) je vous parle “d'événements simultanés” et “d'événements urgents simultanés” ; j'aurais aussi bien pu dire “événements synchrones” et “événement synchrones express”. Par ailleurs, ces vecteurs sont sans effet sur les asynchrones qu'ils soient express ou pas.
Les commandes EVERY
et AFTER
du BASIC créent des “tickers” synchrones (non express). Le BASIC vous donne accès à 4 “tickers” de priorités différentes (le plus prioritaire est le numéro 3).
C'est la boucle de l’interpréteur du BASIC qui s'occupe de lancer les événements synchrones via les vecteurs du kernel. En effet, le terme “synchrone” signifie que les événements ne sont exécutés qu'à votre demande, lorsque vous le décidez. Contrairement aux événements asynchrones qui s'exécutent forcément à la fréquence définie. Voyons maintenant comment demander le lancement des événements synchrones.
Il faut pour cela utiliser les trois vecteurs &BCFB
, &BCFE
et &BD01
:
&BCFB
: KL Next Synchronous&BCFE
: KL Do Sync&BD01
: KL Done SyncC'est comme ça que le BASIC opère ; évidemment la boucle ne fait pas que s'occuper des événements. Pour le reste, c'est le système qui gère les retards et les priorités, j'en ai déjà parlé.
Le Prog 2 met en pratique ce principe avec un seul événement qui est un “ticker” synchrone.
Tout est beaucoup plus simple pour les événements asynchrones. Sachez quand même qu'ils ont tous la même priorité. Les bits 1 à 4 de la classe sont ignorés mais ils peuvent être express ou non. Ceci est illustré par le Prog 3.
Dernier point : quand le Z80 entame l'exécution d'une routine d'interruption via le kernel, il est en EI (c'est-à-dire que les interruptions sont autorisées). Donc, si votre routine est longue, il vaut mieux faire un DI au début (le EI en fin est facultatif, le kernel s'en occupe) afin d'éviter les empilements intempestifs… mais attention, vous risquez alors de perturber l'ensemble des interruptions et événements !
Je vous ai rajouté en dernière minute le Prog 4 qui met en oeuvre un “fast ticker” et vous montre qu'on peut très bien construire soi-même le bloc d'événement sans passer par les vecteurs. Traîne aussi une description à mon avis plus exacte du vecteur &B921
que celle du chapitre précédent.
Voilà, c'en est fini de cette section sur la gestion d'interruptions par le kernel. J'espère que vous aurez compris à peu près !
1 ' Prog 1 2 ' Lancer puis interrompre pendant un 3 ' temps plus ou moins long par 1 appui 4 ' sur Esc puis faire repartir en appuyant 5 ' sur une autre touche. 6 ' Observez bien, c'est très instructif. 7 ' 8 EVERY 2,0 GOSUB 13 9 EVERY 2,1 GOSUB 14 10 EVERY 2,2 GOSUB 15 11 EVERY 2,3 GOSUB 16 12 GOTO 12 13 PRINT"0";:RETURN 14 PRINT"1";:RETURN 15 PRINT"2";:RETURN 16 PRINT"3";:RETURN
Télécharger le listing au format Maxam 1.14
; Prog 2 ; Ticker synchrone ; Org &8000 limit &80ff Nolist ld hl,bloc+6 ; attention !!! ld b,%00001001 ld c,0 ld de,routine call &bcef ; KL INIT EVENT ld hl,bloc ld de,1 ld bc,4 call &bce9 ; KL ADD TICKER ld b,0 Boucle push bc halt ; Faites ici ce que halt ; vous voulez... call &bcfb ; KL Next Synchronous jr nc,pasevent ld bc,&7f00 out (c),c ld c,64+18 out (c),c ld b,32 djnz $ ld bc,&7f44 out (c),c push hl ; sauve l'adresse du bloc d'événement ; de l'événement à traiter call &bcfe ; KL DO Synchronous pop hl xor a call &bd01 ; KL Done Synchronous PasEvent pop bc djnz boucle halt ld hl,bloc call &bcec ; KL DEL TICKER ret ; Routine push af push hl push bc ld bc,&7f10 out (c),c ld c,64+11 out (c),c ld h,&c0 ld a,(bloc+8) ; prend la valeur du compteur 8 bits ld l,a ld a,r ld (hl),a ld b,20 Bouc ds 60 djnz bouc ld bc,&7f44 out (c),c pop bc pop hl pop af ret ; Bloc de controle suivi du bloc d'événement Bloc ds 6+7
Télécharger le listing au format Maxam 1.14
; Prog 3 ; Déclare 2 événements asynchrone Frame Fly ; avec des priorités différentes ; => la priorité n'est pas effective sur les async ; Org &8000 Nolist ld hl,bloc1 ld b,%10000001 ; Priorité 0 ld c,0 ld de,routine1 call &bcd7 ; KL NEW FRAME FLY ld hl,bloc2 ld b,%10011111 ; Priorité 15 ld c,0 ld de,routine2 call &bcd7 ; KL NEW FRAME FLY call &bb18 ; Attend l'appui d'une touche ld hl,bloc1 call &bcdd ; KL DEL FRAME FLY ld hl,bloc2 call &bcdd ; KL DEL FRAME FLY ret ; Routine1 push bc ld bc,&7f10 out (c),c ld c,64+11 out (c),c ; raster blanc call attente ld bc,&7f44 out (c),c pop bc ret Routine2 push bc ld bc,&7f10 out (c),c ld c,64+18 out (c),c ; raster vert call attente ld bc,&7f44 out (c),c pop bc ret Attente ld b,30 boucle ds 60 djnz boucle ret ; Bloc de controle suivi du bloc d'événement Bloc1 ds 2+7 ; Bloc de controle suivi du bloc d'événement Bloc2 ds 2+7
Télécharger le listing au format Maxam 1.14
; Prog 4 ; Fast-Ticker asynchrone ; (sans passer par le vecteur BCE0) Org &8000 Nolist ; ld hl,event_block call &bce3 ; KL ADD FAST TICKER ; call &bb18 ld hl,event_block call &bce6 ; KL DEL FAST TICKER ret ; Routine push bc ld bc,&7f10 out (c),c ld c,64+18 out (c),c push af ld a,r ld (&c000),a ld b,64 djnz $ ; attente ld bc,&7f44 out (c),c pop af pop bc ret ; ; Bloc d'événement ; Event_Block ; bloc de controle dw 0 ; partie commune dw 0 db 0 ; compteur db %10000011 ; classe dw routine db 0