Table des matières

Le Sommaire du Quasar Net

Programmation système

Cet article serait à enrichir avec de nouveaux sujets ; nous n'avions que peu traité du système dans Quasar CPC. Avis aux amateurs.

Content ! 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.

Les RSX

Basé sur l'article publié dans Quasar CPC numéro 9, Initiation à l'Assembleur, par SNN.

Remanié1) par OffseT.

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 !

Un peu de théorie

Reptiles Suppliant une eXtension 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 :

  1. l'adresse du nom de la RSX au format chaîne standard du système ; c'est-à-dire avec le bit d'arrêt (le bit 7) positionné pour le dernier caractère de la chaîne (le fameux STR de Maxam fait ça tout seul pour vous),
  2. le 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.

Un peu de pratique

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.

Vous voyez, ce n'était pas si compliqué, les RSX ne vous ont pas (encore) mangé !

        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

Un peu de silence

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.

Le Kernel : interruptions et événements

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 !

Le concept

Quel événement se cache derrière la porte ? 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”.

Structure d'un bloc

Voici la structure de la partie commune à toute sorte d'événement (appelé bloc d'événement) :

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 :

Les vecteurs

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”.


fRame, fRame, fRameurs, fRamez ! On n'avance à rien dans ce canoé.


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”.


On commence à être bien chaussés !


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.).

Mise en pratique

Avant propos

Alors, nous avons ici trois solutions aqueuses différentes, n'est-ce-pas... 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.

Des détails

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” :

Hein ? Ces calculs t'échappent ?

  1. La routine ne sera jamais lancée si :
    • tick count = 0
    • reload count quelconque
  2. La routine n'est lancée qu'une seule fois si :
    • tick count = 1
    • reload count = 0

Les vecteurs

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 :

Ohé ! Les aminches !

C'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 !

Listings

Programme 1

Télécharger le listing BASIC

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

Programme 2

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

Programme 3

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

Programme 4

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

Documentations externes

1) corrections, précisions
2) qui sont abordées de façon plus explicite dans le dossier sur les ROMs
3) au début du balayage vidéo
4) au milieu du balayage vidéo
5) 6 fois par balayage vidéo