Programmation Assembleur Z80/Initiation
Programmation d'initiation en Z80
modifierPréambule
modifierCette 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
modifierLorsqu'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
- #FB est l'opcode de l'instruction EI
- #C9 est l'opcode de l'instruction RET
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
modifierVoici 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
modifierUn 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
modifier; 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
modifierPour 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
modifierDans 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
Itération de CPIR | Adresse de l'octet comparé | Valeur de HL en sortie |
---|---|---|
1 | #8000 | #8001 |
2 | #8001 | #8002 |
3 | #8002 | #8003 |
4 | #8003 | #8004 |
5 | #8004 (comparaison ok!) | #8005 |
Comparaison de deux chaines de caractères
modifier; 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
- flag Z positionné à 1 si les chaines sont identiques
- flag Z positionné à 0 et flag C à 1, alors la première chaine est plus "forte"
- flag Z positionné à 0 et flag C à 0, alors la seconde chaine est plus "forte"