ASMtrad CPC

Apprenez l'assembleur Z80

Calculer la ligne inférieure

Bravo !!! Vous avez tous réussi le précédent exercice et affiché vos premiers octets à l'écran !!!
Pour cela vous vous êtes servit d'adresses écran que vous aviez noté.
C'est bien, mais...

Imaginez que vous vouliez déplacer votre sprite sur l'écran. Il devient évident que votre routine ne fonctionnera pas puisqu'elle dépend directement des adresses que vous lui avez donné.
Alors il devient évident qu'il va falloir faire autrement...

Pour déplacer un sprite à l'écran, le plus intéressant serait de ne donner l'adresse que du premier octet et, que tout le reste se calcule tout seul.
Ainsi, vous donneriez l'adresse de début d'affichage et votre routine se débrouillerait pour calculer les adresses des autres lignes.

C'est donc ce que nous allons faire maintenant.


Calculons l'adresse inférieure:

Quand vous avez cherché sous BASIC les adresses des différentes lignes vous avez trouvé les lignes suivantes:

#C000
#C800
#D000
#D800
#E000
#E800
#F000
#F800

Regardons donc l'écart entre chaque adresse:

#C800-#C000=#0800
#D000-#C800=#0800
...

Vous pouvez le faire pour toutes les adresses des 8 premières lignes, ca sera pareil: l'écart est de #0800 !!!

On en conclue donc que pour passer à la ligne inférieure il suffit d'ajouter donc #0800.

Prenons notre routine:

                            ORG        #8000
                            LD         A,%00010000      ;notre octet à envoyer
                            LD         (#C000),A        ;on envois à l'adresse #C000 ce que contient A
                            RET                         ;on arrete là

Tout le problème ici est que nous donnons directement l'adresse. On ne pourra pas additionner #0800 la dessus.
Méttons donc notre adresse dans un reg 16bits (Dans un reg 8 bits ca ne rentrera pas... Relisez les précédents cours si vous ne voyez pas pourquoi).

                            ORG        #8000
                            LD         A,%00010000      ;notre octet dans A
                            LD         DE,#C000         ;notre adr dans DE
                            LD         (DE),A           ;on envois à l'adresse contenue dans DE l'octet A
                            RET                         ;on s'arrete là

Jusqu'ici rien de bien compliqué...
Mais ca va tout changer pour nous :)

Maintenant que notre est adr est dans un registre on va pouvoir travailler dessus.
Nous voulons donc ajouter #0800 à notre adresse. Notre adresse est dans DE.

Ajouter #0800 à DE ca revient à ajouter #08 à D. Pourquoi ?

Quand vous faites LD DE,#C000, #C0 se met dans D et #00 se met dans E.
Sinon prenez un autre exemple avec #C850 dans DE, on aurait alors #C8 dans D et #50 dans E !!!
Compris ?

Ajoutons donc #08 à D.

Pour additionner une valeur 8 bits nous allons utiliser l'instruction ADD A,valeur

Comme je vous l'ai dit précédement, toute opération sur 8 bits passera par le registre A.
Hors, si vous faites un ADD A,valeur, la valeur est donc ajoutée à A.

Puisque nous voulons ajouter #08 à D, mais qu'on ne peut passer que par A pour faire une addition, il nous faut donc mettre D dans A...

Rien de plus simple !!! Utilisons l'instruction: LD A,reg

Ainsi:

LD A,D Recopie D dans A.
Rappelez vous que LD charge dans un registre une valeur. Ici on charge donc dans A la valeur contenue dans D !!!

Maintenant que notre A contient bien D et donc notre #C0, nous pouvons ajouter #08 avec notre ADD valeur

                            LD         A,D              ;on copie D dans A
                            ADD        A,#08            ;on ajoute #08 à A

C'est bien, mais nous il faut que notre adresse soit dans DE... Remettons A dans D du coup.

                            LD         A,D
                            ADD        A,#08
                            LD         D,A              ;on met dans D ce qu'il y a dans A

DE contient donc la nouvelle adresse !!!

Reprenons donc notre routine complète:

                            ORG        #8000
                            LD         A,%00010000      ;notre octet dans A
                            LD         DE,#C000         ;notre adr dans DE
                            LD         (DE),A           ;on envois à l'adresse contenue dans DE l'octet A
                            LD         A,D              ;on recopie D dans A
                            ADD        A,#08            ;on ajoute #08 à A
                            LD         D,A              ;on recopie A dans D
                                                        ;DE contient la nouvelle adresse
                            RET                         ;on s’arrête là

On pourrait donc à la suite de notre routine d'addition mettre les autres octets...

                            ORG        #8000
                            LD         A,%00010000      ;notre octet dans A
                            LD         DE,#C000         ;notre adr dans DE
                            LD         (DE),A           ;on envois à l'adresse contenue dans DE l'octet A
                            LD         A,D              ;on recopie D dans A
                            ADD        A,#08            ;on ajoute #08 à A
                            LD         D,A              ;on recopie A dans D
                                                        ;DE contient la nouvelle adresse
                            LD         A,%00111000      ;notre nouvel octet de gfx
                            LD         (DE),A
                            RET                         ;on s’arrête là

Etc etc...

Sauf que... Mettez vous un peu plus bas... Genre en #D000
Exécutez votre routine et regardez le résultat...

Ça ne marche pas !!!

Notre routine marche bien pour les 8 premières lignes mais au passage de la 8ème à la 9ème rien ne va plus...
Regardons donc l'adresse de la ligne 8: #F800

Si on ajoute #0800 à cette adresse on se retrouve en : #0000 !!!
Pourquoi #0000 ? Parce qu'on est dans un registre 16 bits !!!
Sa valeur ne peut donc pas dépasser #FFFF et il boucle donc à 0...

Hors #0000 on n'est simplement plus dans l'écran... L'écran je vous le rappel c'est entre #C000 et #FFFF...

Certains d'entre vous qui se sont amusé à trouver les adresses de toutes les lignes, ont remarqué que la ligne 9 avait pour adresse #C050.
C'est donc cette adresse que nous devrions avoir...

Quel est donc l'écart entre #C050 et #F800 ? #C050-#F800 = #C850.
Il suffirait donc d'ajouter #C850 à l'adresse de notre ligne pour avoir la bonne adr puisque #F800+#C850 = #C050

Mais restons dans notre logique d'avoir une routine qui fonctionne partout sur l'écran.
Regardons donc aussi le passage de la ligne 16 à la ligne 17.

La ligne 16 a pour adr #F850. La ligne suivante a pour adresse: #C0A0
Regardons donc l'écart: #C0A0-#F850 = #C850 !!!

Ca marche !!! Pour passer de la dernière ligne d'un bloc à la première ligne du suivant il suffit donc d'ajouter #C850 !!!

Mais comment savoir si nous sommes sur la dernière ligne ou non ???
Reprenons notre routine précédente:

Après avoir affiché notre octet, on ajoutait #0800 à notre adresse pour passer à la ligne suivante.
Cela marchait parfaitement pour 7 lignes (puisque la première on donnait l'adr).
Arrivé au passage de la ligne 8 à la ligne 9: #F800+#0800 = #0000...

Nous avions donc ce qu'on appelle un débordement.

Un débordement qu'est-ce que c'est ?

Comme nous l'avons vu, pour un registre 16 bits sa valeur maximale est #FFFF. Il ne peut donc aller plus loin que cette valeur !!!
Si nous faisons ne serait-ce qu'un +1, ce registre va boucler et revenir à 0.

Heureusement, on peut détecter ce débordement.

Le Z80 possède ce qu'on appelle des FLAGS. Flag en Anglais ca veut dire drapeau.

Imaginez donc qu'à chaque fois ou quelque chose de spécial se produit, notre Z80 lève un drapeau pour nous dire: Eh les gars, il s'est passé un truc !!!
Bein justement c'est ce que fait le Z80... Et il a pour cela plusieurs drapeaux.

Il en existe un pour nous dire si une valeur est à Zéro: le flag Z.
Un autre pour nous dire s'il y a un débordement le flag C (C comme Carry: There is a Carry=il y a eut un report).
Et quelques autres que nous verrons plus tard.

Ce qui nous intéresse dans le cas présent c'est le flag C parce que justement, nous avons un débordement et que le flag C est mis dès que cela arrive !!!


Comment tester un Flag ?

Pour tester un flag il n'y a pas 36 solutions. Notre Z80 n'en a que quelques-unes et elles passent toutes par une instruction de saut.

Une instruction de saut comme son nom l'indique c'est une instruction qui permet de sauter à une adresse.
Il en existe plusieurs type dont les JP (jump=sauter); CALL (appeler); DJNZ (on verra ca très bientôt) et JR (pareil que JP mais en relatif et on verra cela plus tard aussi...).
Mais ce n'est pas tout !!!

On peut tester les flags avec une instruction que nous connaissons déjà: l'instruction RET !!!
En effet nous pouvons ajouter des conditions à un RET. Et cette condition passe par le test des flags ce qui va nous arranger.

Ainsi nous pouvons dire: RET C: cela signifie que l'instruction RET sera exécutée si et seulement si le flag C est mis !!!

Nous pouvons aussi tester si le flag n'est pas mis (donc pas de débordement): RET NC (NC pour Non Carry).
Et c'est justement ce qui va nous arranger :)

Mais d'abord il nous reste quelque chose à voir:


Déporter une routine pour l'appeler ensuite:

Si dans notre calcul de la ligne inférieure, on teste le flag C pour savoir si on tombe dans le cas d'une adresse qui, ajoutée de #0800 a fait déborder notre registre 16 bits, on utilise un RET C ou NC, notre RET va simplement mettre fin à notre programme... RET retourne où nous étions avant et en l’occurrence c'était le BASIC...
Il nous faut donc déporter notre routine de calcul.

Pour cela nous allons introduire 2 notions: Les Labels et les sauts !!!


Les sauts:

Pour sauter à une adresse nous avons comme je l'ai dit juste au dessus plusieurs solutions et je vais donc en détailler quelques unes.


Les sauts définitifs: JP !!!

Si vous utilisez un JP, vous sauterez à une adresse de façon définitive. Vous y sautez et hop vous continuez là ou vous avez sauté.

La syntaxe est: JP adresse

Vous pouvez là aussi mettre des conditions de la façon suivante: JP NC,adr / JP C,adr / JP Z,adr / JP NZ,adr ...
Il y en a d'autres mais on en parlera plus tard.


Les sauts avec retour: CALL !!!

Imaginez, vous êtes en plein milieu d'une routine, mais vous voulez aller faire autre chose pour ensuite reprendre la ou vous en étiez...

Le CALL permet de faire cela !!! Quand vous ferez un CALL adresse vous sauterez à l'adresse donnée qui sera éxecutée jusqu'à ce qu'il y ait un ... RET !!!

Quand le RET sera exécuté vous reviendrez à ce qu'il a juste après le CALL.
Ainsi vous aurez fait un saut avec retour !!!

Comme pour le JP vous pouvez y ajouter des tests de flags: CALL C,adr / CALL NC,adr / CALL Z,adr / CALL NZ,adr ...
Là encore, d'autres possibilités existent avec d'autres flags comme po ou pv mais on en parlera plus tard.


Déportons donc notre routine de calcul !!!

On reprend notre code:

                                ORG        #8000
                                LD         A,%00010000            ;notre octet dans A
                                LD         DE,#C000               ;notre adr dans DE
                                LD         (DE),A                 ;on envois à l'adresse contenue dans DE l'octet A
                                LD         A,D                    ;on recopie D dans A
                                ADD        A,#08                  ;on ajoute #08 à A
                                LD         D,A                    ;on recopie A dans D
                                                                  ;DE contient la nouvelle adresse
                                LD         A,%00111000            ;notre nouvel octet de gfx
                                LD         (DE),A
                                RET                               ;on s’arrête là

Nous allons commencer par déporter notre routine de calcul:

                                ORG        #8000
                                LD         A,%00010000            ;notre octet dans A
                                LD         DE,#C000               ;notre adr dans DE
                                LD         (DE),A                 ;on envois à l'adresse contenue dans DE l'octet A
                                CALL       CALCUL
                                LD         A,%00111000            ;notre nouvel octet de gfx
                                LD         (DE),A
                                RET                               ;on s’arrête là
     
                CALCUL          LD         A,D                    ;on recopie D dans A
                                ADD        A,#08                  ;on ajoute #08 à A
                                LD         D,A                    ;on recopie A dans D
                                                                  ;DE contient la nouvelle adresse
                                RET

Relisons donc notre source dans l'ordre...

On prend l'octet et on l'envois à l'écran.
Puis on fait un CALL ... Tiens mais c'est quoi ce "CALCUL" ???

C'est ce qu'on appelle un LABEL


Les labels:

Normalement comme je l'ai dis, après un CALL on devrait donner une adresse de saut.
Sauf que donner une adresse de saut ca demande de connaitre l'adr de celui-ci et que bien évidement on ne la connait pas forcément directement

Mais revoyons un peut ce qui se passe à l'assemblage de notre source pour bien comprendre:

Une fois notre source tapé nous l'assemblons avant de l'executer.

Notre assembleur va donc lire notre source et transformer les instructions en binaire (la seule chose que comprend notre Z80) et mettre ces instructions les unes après les autres en RAM à partir de l'adresse qu'on lui a donné avec le ORG adr.

Plus nous avançons dans notre code plus l'adresse avance donc avec lui.

Ainsi si nous voulons sauter quelque part dans notre code il faut connaitre l'adr de l'endroit ou nous voulons sauter.
Mais il faudrait donc compter à partir de notre ORG, la place que prend chaque instruction, pour connaitre l'adr de la routine ou nous voulons sauter...
Ce serait bien entendu bien pénible, d'autant plus si on modifie nos routines par la suite, vu que cela changerait alors les adresses...

Mais heureusement pour nous notre assembleur comprend les labels !!!

Un label, c'est simplement un mot qu'on va mettre à un endroit.
Notre assembleur quand il va assembler notre source va rencontrer le label et noter l'adresse de l'instruction qui le suit comme correspondant à ce label.

Ainsi, quand dans notre source nous écrivons:

                CALCUL          LD         A,D

Le Z80 notera que CALCUL est égal à l'adresse ou se trouve le LD A,D dans la RAM.

Et la vous avez compris l’intérêt: quand on fera un CALL vers un label, on sautera donc justement à l'adresse de l'instruction qui s'y trouve :)

Maintenant que nous avons vu les détails, venons en à notre calcul définitif de la ligne inférieure.
Je reprends juste la routine de calcul pour le moment:


Calcul suite et fin:

                CALCUL          LD         A,D                    ;on recopie D dans A
                                ADD        A,#08                  ;on ajoute #08 à A
                                LD         D,A                    ;on recopie A dans D
                                                                  ;DE contient la nouvelle adresse
                                RET

Il nous faut donc maintenant tester si on a un débordement ou non après notre addition de #08...

Puisque lorsqu'il n'y a pas de débordement notre routine d'addition de #08 suffit, on peut donc arrêter la routine là et retourner à la suite.
Hors on l'a vu, pour retourner à la suite, puisque nous sommes arrivé là par un CALL, il suffit de faire un RET.

Comme on ne veut sortir de la routine QUE lorsqu'il n'y a pas de débordement, il suffit de tester s'il n'y a pas de débordement. On utilise donc: RET NC !!!

                CALCUL          LD         A,D                    ;on recopie D dans A
                                ADD        A,#08                  ;on ajoute #08 à A
                                LD         D,A                    ;on recopie A dans D
                                                                  ;DE contient la nouvelle adresse
                                RET        NC

Mais si il y a débordement il va se passer quoi alors ?

Et bien, notre Z80 va continuer juste après le RET NC à exécuter ce qu'il y a après (même si vous n'y avez rien mis pour le coup...).
Cela signifie surtout que si la carry est mise, il y a eut débordement et on continue le code...

Méttons-y donc notre routine de calcul de la ligne inférieure en cas de débordement.

On a vu plus haut que dans ce cas il fallait ajouter #C850 à notre adresse...
Mais il faut quand même ne pas oublier qu'on a déjà ajouté #0800 à notre adresse... Il faudra donc ajouter #C050 !!!

#C050 c'est une valeur 16 bits, nous ne pouvons donc pas l'ajouter avec une addition 8bits.

Heureusement pour nous on peut aussi faire des additions sur 16 bits.

Comme pour les registres 8bits ou on a vu que toutes les opérations passent par A, pour les registres 16bits, il faut savoir que toutes les opération passent par HL.

Comme notre adresse est dans DE, il faut la mettre dans HL (on aurait aussi pu changer la chose et mettre notre adresse dans HL dès le début ce qui aurait été mieux dans un sens... Mais vous comprendrez dans le cours suivant pourquoi j'ai choisi DE).

Pour mettre notre DE dans HL nous avons plusieurs choix:

LD L,E recopie E dans L

LD H,D recopie D dans H

Notez que la copie se fait de E vers L et non pas vers H justement pour respecter l'adresse.
En effet, l'adresse est par exemple #C850.
D contient #C8 et E contient #50.
On parle de poids fort pour D et de poids faible pour E.
D fournissant l'octet fort de l'adresse (de #0000 à #FF00) alors que E est le poids faible (de #00 à #ff).

Il faut donc respecter poids fort et poids faible !!! C'est très important alors faites attention à ce point.

Notre solution de copie fonctionne mais on a une autre instruction bien pratique qui marche aussi bien et est plus rapide: EX HL,DE (certains assembleur prefèrent EX DE,HL donc si il vous indique une erreur essayez l'un ou l'autre).

EX HL,DE échange le contenu de HL et DE tout simplement. HL devient DE et DE devient HL...
Nous choisirons donc cette instruction.

Une fois notre adresse mise dans HL, il faudra lui additionner #C050 comme nous l'avons dit.
Sauf qu'il n'existe pas d'instruction du type ADD HL,valeur...
Il existe par contre une instruction ADD HL,reg16bits.
Nous allons donc nous en servir.

Mettons donc #C050 dans le registre BC (la encore c'est un choix que vous comprendrez plus tard) et reprenons notre routine de calcul en ajoutant tout ce qu'on a dit...
Vous allez voir c'est simple :)

                CALCUL          LD         A,D                    ;on recopie D dans A
                                ADD        A,#08                  ;on ajoute #08 à A
                                LD         D,A                    ;on recopie A dans D
                                                                  ;DE contient la nouvelle adresse
                                RET        NC
                ;si débordement on continue donc ici et cela signifie qu'on doit ajouter #C050 à notre adresse
                                EX         HL,DE                  ;on a besoin que notre adresse soit dans HL pour pouvoir lui additionner quelque chose
                ;l'adresse est maintenant dans HL
                                LD         BC,#C050               ;on met #C050 dans BC
                                ADD        HL,BC                  ;on additionne HL et BC
                ;HL contient maintenant l'adresse de la ligne inférieure mais on la veut dans DE
                                EX         HL,DE                  ;on refait l'échange et DE contient donc l'adr
                                RET                               ;c'est fini, on retourne d'où on vient !!!

Et Voila, vous pouvez maintenant placer votre sprite là ou vous le voulez sur l'écran, juste en changeant l'adresse de départ :)
Elle n'est pas belle la vie ?