Programmation Assembleur Z80/Initiation

Programmation d'initiation en Z80

modifier

Préambule

modifier

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

modifier

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

  • #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

modifier

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

modifier

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

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

modifier

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

xor a ; obligé d'en mettre un deuxième en cas d'appel direct!

strcpy_loop
cp (hl)
ldi
jr nz,strcpy_loop
ret

Calcul de longueur d'une chaine

modifier

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
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"