Programmation Assembleur Z80/Initiation
= Programmation d'initiation en Z80 =
Préambule.
Cette section pour débutants présente des fonctions simples et utiles, de complexité abordable et dont l'exécution est détaillée pas à pas.
Désactiver le système.
Lorsqu'on exécute un programme Z80 à partir du système, on peut avoir besoin de toute la mémoire ou de toute la puissance du processeur. Le système est presque toujours située à l'adresse d'interruption du mode d'interruption IM 1, à savoir #38. Pour désactiver le système, il faut supprimer la routine située en #38, à minima l'appel à cette fonction.
 DI ; on coupe les interruptions
 LD HL,#C9FB ; HL contient les deux instructions EI : RET
 LD (#38),HL ; écrire le code EI : RET en #38
 EI ; on peut autoriser à nouveau les interruptions
 RET
La valeur #C9FB peut sembler obscure car nous n'avons pas encore parlé d'opcode
Les deux valeurs sont inversées car l'écriture 16 bits est aussi inversée en mémoire (c'est le mode de fonctionnement en big-endian du processeur).
On aurait pu écrire de façon plus "lourde" le code suivant
 DI
 LD HL,desactive_sys
 LD DE,#38
 LD BC,desactive_sys_fin-desactive_sys
 LDIR
 EI
 RET
 desactive_sys
 EI
 RET
 desactive_sys_fin
Avec cette écriture, il est inutile de connaitre les opcodes des instructions ou leur longueur et on recopie de façon générique une portion de code à l'adresse #38
Effacer une zone mémoire.
Voici un sous-programme simple pour initialiser une zone mémoire. Nous assumons deux paramètres en entrée: HL pour l'adresse de la zone à effacer et BC pour la longueur.
 ; HL=destination
 ; BC=longueur
 ; DE modifié
 LD (HL),0 ; effacer le premier octet
 LD D,H ; copier HL dans DE
 LD E,L
 INC DE ; incrémenter DE
 LDIR ; copie en boucle de l'adresse courante vers adresse +1
 RET ; retour à l'appelant
Pour rappel, l'instruction LDIR peut êre assimilée au micro-code suivant:
 copymem
 LD A,(HL)
 INC HL
 LD (DE),A
 INC DE
 DEC BC
 LD A,B
 OR C
 JR NZ,copymem
Ce programme n'est pas très efficient puisque nous réalisons en réalité une copie mémoire pour effacer la zone. Comment pourrions-nous écrire une initialisation simple?
 ; HL=destination
 ; BC=longueur inférieure à 65536-256
 ; A modifié
 memset
 XOR A ; A=0
 INC B ; ajuste le compteur pour le double comptage
 memsetloop
 LD (HL),A ; écrit 0 à l'adresse pointée par HL
 INC HL ; incrémente HL
 DEC C ; décrémente C
 JR NZ,memsetloop ; boucle tant que C est non nul
 DJNZ memsetloop ; décrémente B et reboucle tant que B est non nul
Ce programme est moins rapide que la boucle de copie car le LDIR est une instruction tout-en-un très performante.
Pour aller plus loin
Il est possible de faire mieux en utilisant la pile, sous contrainte. La sauvegarde d'un registre dans la pile est très rapide et on y écrit deux octets à la fois. L'idée est de modifier l'adresse de la pile pour effacer la zone mémoire en exécutant autant de "sauvegarde" que nécessaire. En fait de sauvegarde de registre, on écrira en boucle un registre initialisé à zéro. Cette technique a été très utilisée dans les jeux vidéos pour effacer rapidement un écran. Comme on utilise la pile pour faire nos écritures, il est fortement conseillé de désactiver les interruptions.
 ; HL=destination+longueur
 ; BC=longueur
 ; DE modifié
 memset
 DI ; couper les interruptions pour éviter une corruption mémoire
 LD (savsp+1),SP ; enregistrer la position de la pile en fin de sous-programme
 LD SP,HL ; pointeur de pile sur la fin de l'adresse à effacer
 LD DE,0 ; DE à zéro sera notre valeur pour effacer la mémoire
 INC B ; ajuster le compteur pour double boucle
 razmem
 repeat 32 ; macro assembleur pour écrire 32 fois PUSH DE
 PUSH DE ; envoie une valeuru 16 bits de zéro dans la pile
 rend
 DEC C
 JR NZ,razmem
 DJNZ razmem
 savsp LD SP,#1234 ; restaurer la valeur de notre pointeur de pile
 EI ; autoriser à nouveau les interruptions
 RET
Manipulations de chaines de caractères.
Un bon exercice est de réaliser un jeu de fonctions de manipulation de chaine. Les noms des fonctions suivantes sont dérivées du langage C. On commencera avec des fonctions de copie de chaine (pour rappel une chaine est une suite de caractères terminée par un octet à zéro), des fonctions de comparaison de chaine, de calcul de longueur de chaine puis on pourra se servir de ces fonctions pour créer des meta-fonctions, de recherche d'une chaine dans une autre, de concaténation de chaine, etc. L'idée est de partir d'une ou plusieurs fonctions simples et de les combiner pour réaliser des algorithmes de plus en plus complexes.
Recopie d'une chaine de caractères.
 ; HL=chaine source
 ; DE=chaine destination
 strcpy
 xor a
 strcpy_loop
 ldi
 cp (hl)
 jr nz,strcpy_loop
 ldi
 ret
Le programme ci-dessus est très simple. L'instruction LDI copie un octet de (HL) vers (DE) puis incrémente HL et DE. Le registre BC est aussi décrémenté mais on ne s'en sert pas. L'instruction CP (HL) prendra la valeur de l'octet contenu dans (HL) pour le comparer au registre A qui vaut zéro. Ainsi, quand la valeur de (HL) sera nulle, le flag Z sera positionné. Notre programme boucle tant que NZ, soit non zéro.
Une particularité de l'instruction LDI est de ne pas modifier le flag Z. On peut donc réduire la taille de notre programme et même l'améliorer en cas de chaine vide. Car pour le moment, si on veut copier une chaine vide, HL est incrémenté avant la première comparaison et on ratera le zéro! La comparaison étant faite avant la copie, il n'y a plus besoin de copier le zéro après le dernier test. Notre programme devient donc:
 ; HL=chaine source
 ; DE=chaine destination
 strcpy
 xor a
 strcpy_loop
 cp (hl)
 ldi
 jr nz,strcpy_loop
 ret
Concaténation d'une chaine de caractères à une autre.
Pour concaténer une chaine de caractère, il faut trouver la fin de la première chaine, puis réaliser à partir de là une copie de la seconde.
 ; HL=chaine destination
 ; DE=chaine à concaténer
 strcat
 xor a
 ld b,a
 ld c,a ; plus rapide et plus compact qu'un LD BC,#0000
 cpir
 dec hl
 ex hl,de ; car la chaine à concaténer est la source et que HL contenait la destination!
 call strcpy
 ret
L'instruction CPIR compare l'octet dans (HL) au registre A qui ici est nul, incrémente HL puis décrémente BC. L'instruction CPIR continue tant que BC est différent de zéro. Comme nous avons initialisé le registre BC à zéro, cela veut dire que l'instruction CPIR ne s'arrêtera pas avant d'avoir scanné toute la mémoire. Autrement dit, la condition de sortie est avec certitude le terminateur de la chaine. En sortie de l'instruction CPIR le registre HL contient donc la position du zéro plus un. Nous décrémentons alors HL et permutons les registres pour bien copier de HL vers DE.
Factorisons un peu notre code
Plutôt que d'avoir les deux fonctions séparées, on peut fusionner les deux fonctions et ajouter un label pour avoir un point d'entrée strcpy. Nous évitons de faire un CALL+RET et il n'est plus nécessaire d'initialiser le registre A deux fois de suite (lors du calcul de longueur de strcat puis au début du strcpy).
 ; HL=chaine destination
 ; DE=chaine à concaténer
 strcat
 xor a
 ld b,a
 ld c,a ; plus rapide et plus compact qu'un LD BC,#0000
 cpir
 dec hl
 ex hl,de ; car la chaine à concaténer est la source et que HL contenait la destination!
 strcpy
 cp (hl)
 ldi
 jr nz,strcpy
 ret
Calcul de longueur d'une chaine.
Dans la fonction strcat vous avons vu comment se positionner en fin de chaine mais cela ne nous donne pas sa longueur (c'est très pratique de le savoir, parfois). En modifiant légèrement cette fonction et en ajoutant un compteur, rien de plus simple.
 ; HL=chaine source
 ; retourne DE=longueur de la chaine
 strlen
 xor a
 ld de,#FFFF
 strlen_loop
 inc de
 cpi
 jr nz,strlen_loop
 ret
Ce programme est une version décomposée de l'instruction CPIR. En effet, il faut à chaque itération incrémenter le registre DE qui contiendra en retour cette longueur. Le registre DE est initialisé à #FFFF ou -1 car sinon, l'octet qui contient le zéro sera compté.
Même si ce programme parait simple et optimal, il est dommage d'incrémenter un registre 16 bits et de faire un saut conditionnel à chaque octet lu quand l'instruction CPIR fait presque tout toute seule. L'idée est de conserver la valeur de départ et de soustraire la valeur destination.
 ; HL=chaine source
 ; retourne HL=longueur de la chaine
 strlen
 xor a
 ld d,h
 ld e,l ; on copie HL dans DE
 cpir
 scf ; on force la retenue pour créer un -1 supplémentaire avec la soustraction
 sbc hl,de ; on soustrait le pointeur de fin moins un, du pointeur de début, résultat dans HL
 ret
L'instruction CPIR très rapide à réaliser de nombreuses comparaisons permet à cette fonction d'être plus rapide que la précédente, même pour des valeurs de chaine très courtes. Et si on veut avoir un comportement identique à la première fonction, il suffit d'ajouter un EX HL,DE avant le ret pour permuter les deux registres et renvoyer la longueur dans DE.
Par exemple avec une chaine de 4 caractères localisée en #8000 qui aura un zéro terminateur en #8004. L'instruction CPIR s'arrêtera avec HL=#8005 car pour rappel, l'incrémentation du registre HL se fait après la comparaison avec la valeur de l'octet dans (HL). #8005-1-#8000 donne bien 4.
 defb 'glop',0
 #8000 #67 'g'
 #8001 #6C 'l'
 #8002 #6F 'o'
 #8003 #70 'p'
 #8004 #00 \0
Comparaison de deux chaines de caractères.
 ; HL=première chaine
 ; DE=deuxième chaine
 strcmp
 ld a,(de) ; pour comparer les caractères des deux chaines
 sub (hl) ; on réalise une soustraction (sans modifier les chaines)
 ret nz ; si les caractères sont différents, le résultat sera différent de zéro, la retenue indique le résultat
 add (hl) ; les octets sont identiques alors pour tester le zéro on ajoute ce qu'on vient d'enlever
 ret z ; et on sort de la fonction si on est sur le terminateur de chaine (zéro)
 inc de
 inc hl ; on incrémente les pointeurs de chaine pour continuer la comparaison
 jr strcmp ; et on recommence
Positionnement des flags en sortie de fonction