ASMtrad CPC

Apprenez l'assembleur Z80

Déduire la valeur du registre 0 du CRTC.

Détecteur de Chany !
(J’ai eu du mal à trouver une utilité pour ce programme. Ne me jugez pas)

Sur CPC, il est impossible de connaître les valeurs des différents registres du CRTC en interrogeant le composant.
Jusque maintenant ça n’a posé de problème à personne dans la mesure où généralement on reprogramme les valeurs directement.
L’écran peut perdre sa synchronisation quelques millisecondes, mais ensuite tout rentre dans l’ordre.
Depuis 30 ans c’est considéré comme inévitable.
Maintenant qu’on a le temps, l’expérience et le cerveau bien cabossé par des nuits de meetings, on pourrait avoir envie de faire des programmes qui ne servent à rien, juste pour l’amour du code.
C’est ce que je vous propose ici : Nous allons écrire un programme qui déduit la valeur du registre zéro du CRTC.
Cela nous permettra d’aborder les interruptions et la manière dont le Gate Array les gère.

Vous avez déjà sûrement entendu que le CPC dispose d’interruptions à 300 Hz, et qu’elles sont invariables (Hint : C’est faux!).
Comment ça fonctionne ?
Elles résultent des traitements à la fois du CRTC, du Gate Array et du Z80 :
Le CRTC génère les signaux que le Gate Array utilise pour mesurer le temps, et le Z80 utilise celui envoyé par le Gate Array pour lancer ses routines au 300e de seconde.

Le CRTC est responsable de l’émission des signaux Hsync et Vsync.
En pratique, cela correspond a des signaux qui respectivement désignent une fin de scanline, et une fin d’écran.
Les moniteurs s’en servent pour se caler afin d’afficher une image stable.
Sauf que… Sur CPC ces signaux sont émis par le Gate Array, de même que l’image.
Celui-ci est situé entre le CRTC et l’écran et filtre/traite/utilise les signaux émis par le CRTC avant de les envoyer au moniteur.
En clair, le CRTC envoie au Gate Array des messages comme « Là il faut afficher l’octet à l’adresse machin », « Ici il y a du border », « Ah, ma scanline est terminée », « Là c’est la fin de l’écran ».
Tout se fait de manière séquentielle, habituellement 19968 fois par frame (Une image telle qu’affichée sur le moniteur), ce qui revient à 998400 fois par seconde. Presque un million !

Comment fait le Gate Array pour envoyer des interruptions tous les 300e de secondes au Z80 ?
C’est simple : Une image générée par le CRTC fait 312 scanlines et dure un 50e de seconde.
Pour avoir une interruption au 300e il « suffit » de compter les scanlines (En d’autres termes, compter les signaux Hsync émis par le CRTC) et de générer une interruption à l’intention du Z80 toutes les 52 scanlines (300 fois par seconde, c’est 6 fois plus rapide que 50 fois par seconde.
Il faut donc « découper » une frame en 6 pour avoir 300 Hz.
Or, 312/6=52 ! Ça tombe pile!).
Ce compteur de 52 scanlines est relativement fragile. Il existe des méthodes pour le secouer un peu.
Pour les curieux·ses, rendez-vous dans le Compendium de Longshot pour en savoir plus.

Et notre Z80, que fait-il lorsqu’il reçoit une interruption ?
S’il avait désactivé la prise en charge des interruptions (Opcode DI), il fait la sourde oreille.
Sinon, sa première réaction c’est plutôt « Attends, là je fais un truc ! ».
En effet, il termine l’opcode en cours avant de prendre en compte le signal d’interruption. C’est traître car votre appel à la routine d’interruption peut ainsi être décalé de 6, 7, voire une infinité de microsecondes (Et oui, on peut faire sur Z80 des instructions dont l’opcode peut prendre 64 Ko et donc faire boucler le processeur indéfiniment (Peut-être le sujet d’un autre article?).
Celles et ceux qui ont essayé de faire des rasters sous interruptions auront sûrement constaté que les changements de palette se décalent à chaque frame, donnant l’impression d’un tremblement.
C’est à cause du Z80 qui n’en fait qu’à sa tête !

Imaginons que notre Z80 est bien luné, qu’il a terminé son instruction, bref, qu’il est enfin prêt à prendre en compte l’interruption.
Il va alors effectuer l’équivalent d’un RST #38, sauf que le RST en question durera 5 microsecondes au lieu de 4 comme de coutume (Il est probable que la microseconde perdue est due à la prise en compte de l’interruption).
Une routine placée à l’adresse #38 effectuera sa gestion au 300e, et rendra la main au programme interrompu via un « EI : RET », en attendant d’être appelée à nouveau un 300e de seconde plus tard.

Et le registre 0 dans tout ça ?

Oui, nous sommes quand même là pour ça. Comment pourrions-nous le déduire grâce à ce que nous venons d’apprendre ?
Le registre 0 du CRTC définit la longueur d’une scanline.
Celles-ci sont comptées par le Gate Array qui envoie au Z80 une interruption à chaque fois qu’il en a vu passer 52.
En clair, dans le cas général si le registre 0=63 il y a 64 microsecondes par scanline et donc 64*52=3328 entre chaque interruption.
Si le registre 0=64 il y a 65 microsecondes par scanline et donc il y en a 65*52=3380 entre chaque interruption.
En résumé, le temps entre chaque interruption est égal à (R0+1)*52.
Il n’y a donc qu’à mesurer le temps entre deux interruptions pour en déduire la valeur du registre 0 !

Le principe du code est relativement simple : On a deux routines qui seront lancées lors des prochaines interruptions :
- La première, @Start_timer, initialise le compteur (registre A) et modifie le code pour que la prochaine routine appelée sous interruption soit @Stop_timer, qui - comme son nom l’indique – récupère la valeur du compteur, nettoie tout le bazar mis en place par notre routine de comptage et rend la main !
- Entre l’appel de @Start_timer et @Stop_timer on a une routine qui tourne en boucle infinie, @Counting_loop.
A noter, la petite astuce qui consiste à compter les blocs de 52 microsecondes :
Plutôt que d’obtenir en fin de routine un nombre qu’on devra diviser par 52 pour obtenir la durée d’une scanline, autant se passer d’une division et donner directement le résultat.

Il existe d’autres techniques amusantes (Enfin, c’est une question de point de vue) pour déterminer la valeur d’autres registres du CRTC. A vous de trouver les autres !

CheshireCat

            ;==============================================================================
            ; GUESSREG0VALUE
            ;------------------------------------------------------------------------------
            ; Determines the value of the 0 register. Usually it's 63 but some people
            ; might set it to 64 or 62. Bad for the CTM but useable.
            ;
            ; The trick consists in measuring the NOPs number between two halts, which
            ; is 52*(reg0 +1)
            ; for this implementation we will only measure the number of groups of 52 NOPs
            ; between two halts. This way, no need for a division by 52 :-D
            ;------------------------------------------------------------------------------
            ; The result will be stored in reg A
            ;==============================================================================
            macro       GUESSREG0VALUE
                        di
                        ld  hl,(#38)
                        ld  (@SaveInt38+1),hl
                        ld  hl,(#3A)
                        ld  (@SaveInt38_+1),hl
                        ld  hl,#c9fb
                        ld  (#38),hl
                        ei
                        ld  b,#f5
            @WaitFrame:
                        halt
                        in  a,(c)
                        rra
                        jr  nc,@WaitFrame
                        halt
                        ld  hl,@Start_timer
                        ld  a,#C3
                        ld  (#38),a
                        ld  (#39),hl
                        halt
            @Counting_loop:
                        inc a
                        DEFS 52-3-1         ; 3 is the timing of a JR, 1 is the timing of inc A
                        jr  @Counting_loop
            @Start_timer:
                        ld  a,#FF
                        ld  hl,@Stop_timer
                        ld  (#39),hl
                        ei
                        ret
            @Stop_timer:
                        pop hl              ; Cancel the return address generated by the interruption
            @SaveInt38:
                        ld  hl,#c9fb
                        ld  (#38),hl
            @SaveInt38_:
                        ld  hl,0
                        ld  (#3A),hl
            mend