Retourner au sommaire

Initiation au Z80

Cette page est l'assemblage de nombreux articles issus de Quasar CPC et manque un peu d'homogénéïté. En outre, il faudra peut-être scinder cette page en deux ou trois au final.

Le but de cette rubrique est de poser toutes les bases nécessaires afin de se lancer dans la programmation en Assembleur Z80 sur CPC. Nous verrons ainsi ici tout ce qui concerne les instructions, les registres, les adressages, etc..

Préliminaires

Basé sur l'article publié dans Quasar CPC numéro 1, Initiation à l'Assembleur, par OffseT.

Pour bien grandir, mangez du 8 bits tous les jours Chassez que ve jais pous vrendre au berceau1) ! Autrement dit, je considère que vous n'avez jamais vu quelque listing assembleur que ce soit et que vous n'en connaissez aucune commande ! Ok ? C'est parti !

Il est primordial que je commence par vous expliquer la commande LD (prononcer comme Load). Vous devez savoir qu'en assembleur Z80, on ne travaille pas avec des variables mais avec des registres. La différence est que ceux-ci ne peuvent contenir que des entiers et que leur nombre est limité, leur capacité aussi. Ainsi, vous devez également faire la différence entre les registres simples et les doubles.

Les simples ne peuvent contenir que des nombres compris entre 0 et 2552) alors que les doubles peuvent aller jusqu'à 655353). Mais là où ça se corse, c'est quand un registre double est formé par deux registres simples. Non, ne hurlez pas! Voici des exemples : prenons les registres simples B et C ; ils formeront le registre double BC. Et, comme je suis gentil, je vous donne la liste de “tous” les registres doubles existants : AF, BC, DE, HL, IX, IY, etc…

Pour ce premier cours nous ne nous servirons que de A et HL qui sont les plus utilisés. Revenons donc à LD : si vous faites un LD A,10 cela correspond à un A=10 en BASIC. Simple non ? De même, LD HL,65535 mettra 65535 dans HL. Oui mais là c'est un peu plus complexe et il vaut mieux raisonner en hexadécimal4). Ainsi, 65535=&FFFF et on lit alors très simplement que H contient &FF, tout comme L5). De même, si l'on met un LD HL,&01B3 ; H=&01 et L=&B3. Compris ? Parfait ! Maintenant que vous connaissez LD par coeur, je vais vous fournir deux vecteurs6). L'un équivalent à “PRINT CHR$(A)”, c'est le &BB5A et l'autre équivalent à “LOCATE H,L”, c'est le &BB75. Non ! On ne se sert pas de LD pour les exécuter mais de la commande CALL. C'est très simple, il faut un “CALL adresse” et ça marche ! Alors, voici un petit programme qui affiche le code ascii 65 aux coordonnées 1,1 :

         ORG &5000   ;Implantation du prog en &5000
         LD HL,&0101 ;H=1 et L=1
         CALL &BB75  ;LOCATE H,L
         LD A,65     ;A=65
         CALL &BB5A  ;PRINT CHR$(A)
         RET         ;Retour au Basic

Et pour ceusses qui n'ont pas ouvert la doc de leur assembleur, je dis même comment lancer cette routine : sous BASIC faites un CALL &5000… évidemment, après avoir assemblé le programme…

Mais quesquidi ?

Premier programme

Basé sur l'article publié dans Quasar CPC numéro 2, Initiation à l'assembleur, par Zik.

Nous allons ici étudier en douceur le cas d'un tout petit programme qui a l'avantage de mettre en jeu deux éléments essentiels : les boucles et l'accès aux données.

Les boucles

L'épreuve du feu : le premier programme assembleur ! Commençons donc par les boucles. On utilise l'instruction JP (JumP) pour faire un saut dans un programme. Je m'explique : si, par exemple, on veut qu'un programme recommence au début quand son exécution est terminée, on place l'instruction JP DEBUT en fin de programme ; DEBUT étant un label placé au début du prog' dans notre exemple (DEBUT ayant été pris au harsard).

Mais j'y pense, on ne vous a pas parlé des labels ! Bon, je vais vous expliquer. Un label est un mot (ou une lettre) quelconque qui est placé en face d'une commande donnée, il sert à ne pas avoir à préciser l'adresse mémoire où se trouve l'instruction, mais seulement préciser le nom donné au label concerné. Pour mieux vous illustrer cela, vous n'avez qu'à regarder le programme ci-dessous :

       ORG &5000
       LD HL,TEXTE
BOUCLE LD A,(HL)
       CP 0
       RET Z
       CALL &BB5A
       INC HL
       JP BOUCLE
;
TEXTE  DB "Bonjour !",0

En observant le programme, vous avez pu voir l'instruction RET Z (eh oui !). Vous devez connaître RET mais pas Z (qu'est-ce qu'il va encore nous sortir !?). Ce fameux Z signifie “si le flag Z est positionné tu fais l'instruction”. Le flag Z est modifié par certaines commandes dont CP (qui signifie “ComPare” au registre A), ce qui veut dire en simplifiant (dans notre prog') : ComPare A à 0, si A=0 alors RETourne au BASIC, sinon continue.

Nous aborderons plus en détail la question des flags dans la section suivante.

Les data

Il me reste donc à vous parler des data. C'est le même principe qu'en BASIC, on réserve une zone mémoire pour y stocker des données, sauf que DATA est remplacé par DB7) (ou DW8) pour les tableaux d'adresses) et que la commande READ du BASIC est remplacée par des LD. LD HL,TEXTE fait pointer HL sur les données (ici le texte), on lit celles-ci grâce à LD A,(HL) et on incrémente le pointeur (ici INC HL) pour pointer sur la donnée suivante. Vous pouvez déduire que le texte doit être terminé par un 0 et que ce programme correspond au PRINT du BASIC (zai failli oublier de le dire !).

Les flags

Les flags sont des indicateurs9) positionnés automatiquement par certaines instructions du Z80 et qui permettent d'effectuer des opérations conditionnelles (des branchements, des calculs, etc..). Ils sont contenus dans le registre F qui se présente de la forme suivante :

Oh les beaux drapeaux !

 7   6   5   4   3   2   1   0 
S Z X H X P/V N C

Où les flags sont les suivants :

Flag Description
Sflag de signe
Zflag de zéro
Hflag de demi-Carry
P/Vflag de parité/overflow
Nflag Add/Substract
Cflag de Carry

Voyons maintenant comment s'utilisent ces petits drapeaux…

Les branchements

Basé sur l'article publié dans Quasar CPC numéro 3, Initiation à l'assembleur, par Zik.

Nous allons parler des instructions de saut en mémoire paramétrées. Je vais tout d'abord faire un petit rappel sur les fameuses instructions JP et CALL dont on vous a déjà parlé. JP (JumP) peut-être comparé à la commande GOTO du BASIC, à la différence qu'après un GOTO on doit préciser un numéro de ligne alors qu'après un JP on doit spécifier le nom d'un label (qui en fait représente une adresse en mémoire). De même, l'ensemble CALLRET est l'équivalent, en BASIC, du GOSUBRETURN.

Comme je l'ai exprimé précédemment, ces trois instructions (JP, CALL et RET pour ceux qui ne suivent pas) admettent des paramètres qui se substituent aux IFTHENELSE du BASIC. Les tests possibles sont limités, ceux-ci sont en fait réalisés à partir des bits du registres F (pour Flags).

Je vais donc vous dire quels sont les tests disponibles et ensuite voir quelles sont les principales commandes qui modifient ces flags et donc agissent sur ces tests.

Pris en flag ! Voici comment on effectue ces tests :

  • RET flag
  • JP flag,label
  • CALL flag,label

flag peut être Z, NZ, C, NC, ou encore d'autres expression plus complexes et moins utiles que je ne traiterai pas dans cet article.

Commençons par Z (pour Zero). Ce flag indique si l'opération qui précède le test a donné un résultat nul (avec CP, DEC ou les opérateurs logiques par exemple). NZ (pour Non-Zero) est donc le test contraire. C (pour Carry) permet de tester s'il s'est produit un débordement lors de l'opération précédent le test. Je rappelle qu'un débordement s'effectue lorsqu'une instruction faire dépasser à un registre sa valeur maximale ou quand le registre devrait prendre une valeur négative ce qui est impossible (les valeurs maximales sont 255 pour les registres simples et 65535 pour les doubles, 0 est la valeur minimale). À titre d'exemple, la carry sera mise à 1 si l'on fait un LD A,1 suivi d'un SUB A,2 (A=A-2) et A contiendra alors 255 et non -110) : on dit que A a bouclé.

On peut utiliser cela par la comptabilisation sur les doigts des mains (10 en principe, sait-on jamais). On peut donc compter 0 au minimum et 10 au maximum. Donc si l'on doit compter un nombre supérieur à 10, on partira de 1 jusqu'à 10, puis o, retiendra qu'une dizaine a été décomptée (ce qui équivaut à mettre la Carry à 1 pour le Z80) et on repartira à 0, et si besoin est, on bouclera de nouveau (s'il y a plusieurs dizaines juqu'à parvenir au nombre escompté… vu comme ça, on n'a plus trop envie de compter sur ses doigts) ; on voit ici la supériorité inifie de l'homme sur la machine qui peut comptabiliser le nombre de dépassements (le nombre de dizaines) alors que le pauvre Z80 ne peut malgré toute sa bonne volonté signaler qu'un seul dépassement ! On devinera que NC indique qu'aucun débordement n'a eu lieu lors de l'opération précédente. On notera par ailleurs que certaines commandes mettent d'office la Carry à 1 ou à 0, c'est le cas par exemple des opérateurs logiques (mode à 0) ou de SCF (Set Carry Flag) (mise à 1).

Il ne faut pas confondre un beau label et la belle femme ! Vous voulez des exemples ? D'accord, voici un exemple de gestion du flag Z à partir d'un CP :

Exemple1 LD A,R
         CP 162
         JP Z,Label1
         RET
Exemple2 LD A,R
         CP 162
         RET NZ
         JP Label1

Ces deux exemples font exactement la même chose : ils lancent la routine LABEL1 si A est égal à 162 ; R étant un registre interne non contrôlable et que l'on peut considérer (à tort) comme un générateur de nombres aléatoires.

Voyons maintenant un test sur la Carry à partir d'un ADD :

Exemple3 LD A,R
         ADD A,128
         JP C,Label2
         RET

En examinant cette routine, on s'aperçoit qu'elle appelle Label2 si R est supérieur à 127 (car 128+128 est égal à 256 et donc égal à 0 sur 8 bits, il y a débordement).

La mise à jour des flags

Basé sur l'article publié dans Quasar CPC numéro 5, Initiation à l'assembleur, par Zik.

Le tableau ci-dessous indique les modifications que subissent les différents flags en fonctions des instructions de l'assembleur. Comme on me le fait remarquer, il figure les modifications des flags P/V et S dont nous n'avons pas détaillé le fonctionnement dans la section sur les flags pour des raisons obscures.

Ce tableau est très utile quand on veut effectuer un test après une instruction, ne sachant pas si elle modifie effectivement le flag Z ou autre. Les points d'interrogation de la colonne de gauche représentent soit une donnée, soit un registre 8 ou 16 bits, soit une valeur ou une adresse. Toujours dans cette colonne, ”(16 bits)” indique un registre 16 bits (ça paraît logique) et ”AUTRE” tout ce qui n'est pas précisé.

Pour le reste, les astérisques (*) indiquent que le flag en question est modifié, les points (.) qu'il ne l'est pas, et les “0” ou “1” qu'il est mis à 0 ou 1 (et toc !).

Les drapeaux sont très important pour savoir dans quelle galère on s'embarque !

Instruction    S   Z  P/V  C 
ADC ?,? * * * *
ADD A,? * * * *
ADD A,AUTRE . . . *
AND ? * * * 0
BIT ?,? * * * .
CALL . . . .
CCF . . . 0
CP ? * * * *
CPD * * * .
CPDR * * * .
CPI * * * .
CPIR * * * .
CPL . . . .
DAA * * * .
DEC (16 bits) . . . .
DEC AUTRE * * * .
DI . . . .
DJNZ . . . .
EI . . . .
EX ?,? . . . .
EXX . . . .
HALT . . . .
IM ? . . . .
IN ?,? * * * .
Instruction    S   Z  P/V  C 
INC (16 bits) . . . .
INC AUTRE * * * .
IND * * * .
INDR * * * .
INI * * * .
INIR * * * .
JP ? . . . .
JR ? . . . .
LD A,I * * * .
LD A,R * * * .
LD AUTRE . . . .
LDD . . * .
LDDR . . 0 .
LDI . . * .
LDIR . . 0 .
NEG . . . .
NOP . . . .
OR ? * * * 0
OTDR * * * .
OTIR * * * .
OUT ?,? . . . .
OUTD * * * .
OUTI * * * .
Instruction    S   Z  P/V  C 
POP ? . . . .
PUSH ? . . . .
RES ?,? . . . .
RET . . . .
RL ? * * * *
RLA . . . *
RLC ? * * * *
RLCA . . . *
RLD * * * .
RR ? * * * *
RRA . . . *
RRC ? * * * *
RRCA . . . *
RRD * * * .
RST ? . . . .
SBC ? * * * *
SCF . . . 1
SET ?,? . . . .
SLA ? * * * *
SRA ? * * * *
SRL ? * * * *
SUB ? * * * *
XOR ? * * * 0

Les adressages à bannir

Basé sur l'article publié dans Quasar CPC numéro 5, Perfectionnement à l'Assembleur, par OffseT.

Moi, j'ai optimisé mon trésor ! Maintenant que les bases de l'assembleur sont posées, nous allons pour la toute première fois évoquer la notion d'optimisation. En effet, il est des instructions qu'il convient d'éviter car elle n'ont absolument aucun intérêt dans le cas général. Je vous propose ici de lister les plus évidentes.

LD A,0

Tout d'abord, je ne veux plus voir de LD A,0 ! C'est la façon la plus bête de perdre du temps machine… En effet, XOR A est beaucoup plus rapide et, de plus, il prend deux fois moins de place en RAM (un octet contre deux pour LD A,0). Pour ceux qui ne voient pas pourquoi XOR A équivaut à LD A,0, voici l'explication : comme chacun sait, XOR est le OU exclusif dont voici la table de vérité :

XOR  0   1 
0 0 1
1 1 0

Pour plus de clarté (il faut penser à ceux qui n'ont jamais utilisé le binaire) voici un petit exemples :

…ou en décimal…

Comme vous pouvez le constater, il est indispensable de raisonner en binaire pour percevoir les subtilités de l'assembleur. Si vous avez bien suivi ce qui précède, vous devriez comprendre sans problème pourquoi un XOR A met A à zéro… En outre XOR a l'avantage de mettre la carry à 1 ce qui est fort utile dans certains cas. Les seuls cas où l'usage du LD A,0 se justifie, c'est lorsque l'on souhaite ne pas modifier les flags.

CP 0

Et puis il y a aussi le fameux CP 0 ; pour ainsi dire c'est encore plus “grave” que le LD A,0 puisque cette instruction est souvent utilisée dans des boucles d'où, catastrophe ! Le substitut idéal c'est le OR A qui met le flag Z à zéro si A est nul. Je ne vais pas vous faire l'affront de détailler la table de vérité… Passons…

Sachez tout de même que, tout comme l'opérateur logique XOR, OR (et AND aussi d'ailleurs) met la carry à 0.

Le codage des mnémoniques

Basé sur l'article publié dans Quasar CPC numéro 5, Initiation à l'assembleur, par Zik.

Allons dans les entrailles des instructions du Z80... Mais il y a un autre tableau dans ces pages (ô miracle) ! Celui-ci (qui ne s'est d'ailleurs pas saisi tout seul et instantanément) nous montre comment s'effectue le codage en mémoire de toutes les mnémoniques (légales) de l'assembleur.

Quelques explications s'imposent :

  • dans la colonne des mnémoniques :
    • data est une donnée sur 8 bits,
    • data16 est une donnée 16 bits,
    • addr est une adresse 16 bits,
    • disp est un offset signé sur 8 bits,
    • reg est un registre 8 bits,
    • rp est un registre 16 bits11),
    • b est un numéro de bit entre 0 et 7,
  • dans la colonne des codes :
    • les virgules séparent des octets et les espaces des quartets,
    • yy est une donnée 8 bits,
    • ll,hh est une donnée 16 bits stockée en little endian (attention, elle est codée à l'envers : poids faible et poids fort ensuite),
    • xx représente un coupe de bits,
    • sss12), ddd13), rrr14) ou bbb15) représente un triplet de bits.

Voilà, j'ai pratiquement tout dit (bon, d'accord, je n'ai pas dit que toutes les valeurs numériques sont en hexadécimal ou en binaire, que ”port” est un port (!) sur 8 bits et que ”m” peut prendre les valeurs 0, 1 ou 2).

Cette liste de commandes contient toutes celles offertes par le Z80 lui-même mais sur CPC, compte tenu du câblage de celui-ci, IND, INDR, INI, INIR, OTDR, OTIR, OUTD, OUTI et RETN ne fonctionnent pas correctement.

Mnémoniques            Code               
ADC  A,reg 86,1rrr
ADC  A,data CE,yy
ADC  A,(HL) 8E
ADC  A,(IX+disp) DD,8E,disp
ADC  A,(IY+disp) FD,8E,disp
ADC  HL,rp ED,01xx A
ADD  A,reg 8 0rrr
ADD  A,data C6,yy
ADD  A,(HL) 86
ADD  A,(IX+disp) DD,86,disp
ADD  A,(IY+disp) FD,86,disp
ADD  HL,BC 09
ADD  HL,DE 19
ADD  HL,HL 29
ADD  HL,SP 39
ADD  IX,rp DD,00xx 9
ADD  IY,rp FD,00xx 9
AND  reg A 0rrr
AND  data E6,yy
AND  (HL) A6
AND  (IX+disp) DD,A6,disp
AND  (IY+disp) FD,A6,disp
BIT  b,reg CB,01bbbrrr
BIT  b,(HL) CB,01bbb110
BIT  b,(IX+disp) DD,CB,disp,01bbb110
BIT  b,(IY+disp) FD,CB,disp,01bbb110
CALL addr CD,ll,hh
CALL Z,addr CC,ll,hh
CALL NZ,addr C4,ll,hh
CALL C,addr DC,ll,hh
CALL NC,addr D4,ll,hh
CALL PE,addr EC,ll,hh
CALL PO,addr E4,ll,hh
CALL P,addr F4,ll,hh
CALL M,addr FC,ll,hh
CCF 3F
CP   reg B 1rrr
CP   data FE,yy
CP   (HL) BE
CP   (IX+disp) DD,BE,disp
CP   (IY+disp) FD,BE,disp
CPD ED,A9
CPDR ED,B9
CPI ED,A1
CPIR ED,B1
CPL 2F
DAA 27
DEC  A 3D
DEC  B 05
DEC  C 0D
DEC  D 15
DEC  E 1D
DEC  H 25
DEC  L 2D
DEC  BC 0B
DEC  DE 1B
DEC  HL 2B
DEC  SP 3B
DEC  IX DD,2B
DEC  IY FD,2B
DEC  (HL) 35
DEC  (IX+disp) DD,35,disp
DEC  (IY+disp) FD,35,disp
DI F3
DJNZ addr 10,disp-2
EI FB
EX   AF,AF' 08
EX   DE,HL EB
EX   (SP),HL E3
EX   (SP),IX DD,E3
EX   (SP),IY FD,E3
EXX D9
HALT 76
IM   m ED,010nn110
IN   A,(port) DB,yy
IN   reg,(C) ED,01ddd000
INC  A 3C
INC  B 04
INC  C 0C
INC  D 14
INC  E 1C
INC  H 24
INC  L 2C
INC  BC 03
INC  DE 13
INC  HL 23
INC  SP 33
INC  IX DD,23
INC  IY FD,23
INC  (HL) 34
INC  (IX+disp) DD,34,disp
INC  (IY+disp) FD,34,disp
IND ED,AA
INDR ED,BA
INI ED,A2
INIR ED,B2
JP   addr C3,ll,hh
JP   Z,addr CA,ll,hh
JP   NZ,addr C2,ll,hh
JP   C,addr DA,ll,hh
JP   NC,addr D2,ll,hh
JP   PE,addr EA,ll,hh
JP   PO,addr E2,ll,hh
JP   P,addr F2,ll,hh
JP   M,addr FA,ll,hh
JP   (HL) E9
JP   (IX) DD,E9
JP   (IY) FD,E9
JR   addr 18,disp-2
JR   Z,addr 28,disp-2
JR   NZ,addr 20,disp-2
JR   C,addr 38,disp-2
JR   NC,addr 30,disp-2
LD   A,I ED,57
LD   A,R ED,5F
LD   I,A ED,47
LD   R,A ED,4F
LD   A,reg 7 1sss
LD   B,reg 4 0sss
LD   C,reg 4 1sss
LD   D,reg 5 0sss
LD   E,reg 5 1sss
LD   H,reg 6 0sss
LD   L,reg 6 1sss
LD   A,data 3E,yy
LD   B,data 06,yy
LD   C,data 0E,yy
LD   D,data 16,yy
LD   E,data 1E,yy
LD   H,data 26,yy
LD   L,data 2E
LD   A,(addr) 3A,ll,hh
LD   A,(BC) 0A
LD   A,(DE) 1A
LD   A,(HL) 7E
LD   B,(HL) 46
LD   C,(HL) 4E
LD   D,(HL) 56
Mnémoniques            Code               
LD   E,(HL) 5E
LD   H,(HL) 66
LD   L,(HL) 6E
LD   reg,(IX+disp) DD,01ddd110,disp
LD   reg,(IY+disp) FD,01ddd110,disp
LD   BC,data16 01,ll,hh
LD   DE,data16 11,ll,hh
LD   HL,data16 21,ll,hh
LD   SP,data16 31,ll,hh
LD   IX,data16 DD,21,ll,hh
LD   IY,data16 FD,21,ll,hh
LD   SP,HL F9
LD   SP,IX DD,F9
LD   SP,IY FD,F9
LD   rp,(addr) ED,01xx B,ll,hh
LD   HL,(addr) 2A,ll,hh
LD   IX,(addr) DD,2A,ll,hh
LD   IY,(addr) FD,2A,ll,hh
LD   (BC),A 02
LD   (DE),A 12
LD   (HL),reg 7 0sss
LD   (IX+disp),reg DD,7 0sss,disp
LD   (IY+disp),reg FD,7 0sss,disp
LD   (HL),data 36,yy
LD   (IX+disp),data DD,36,disp,yy
LD   (IY+disp),data FD,36,disp,yy
LD   (addr),A 32,ll,hh
LD   (addr),rp ED,01xx 3,ll,hh
LD   (addr),HL 22,ll,hh
LD   (addr),IX DD,22,ll,hh
LD   (addr),IY FD,22,ll,hh
LDD ED,A8
LDDR ED,B8
LDI ED,A0
LDIR ED,B0
NEG ED,44
NOP 00
OR   reg B 0rrr
OR   data F6,yy
OR   (HL) B6
OR   (IX+disp) DD,B6,disp
OR   (IY+disp) FD,B6,disp
OTDR ED,BB
OTIR ED,B3
OUT (port),A D3,yy
OUT (C),reg ED,01sss001
OUTD ED,AB
OUTI ED,A3
POP  AF F1
POP  BC C1
POP  DE D1
POP  HL E1
POP  IX DD,E1
POP  IY FD,E1
PUSH AF F5
PUSH BC C5
PUSH DE D5
PUSH HL E5
PUSH IX DD,E5
PUSH IY FD,E5
RES  b,reg CB,10bbbrrr
RES  b,(HL) CB,10bbb110
RES  b,(IX+disp) DD,CB,disp,10bbb110
RES  b,(IY+disp) FD,CB,disp,10bbb110
RET C9
RET  Z C8
RET  NZ C0
RET  NC D0
RET  C D8
RET  PE E8
RET  PO E0
RET  P F0
RET  M F8
RETI ED,4D
RETN ED,45
RL   reg CB,1 0rrr
RL   (HL) CB,16
RL   (IX+disp) DD,CB,disp,16
RL   (IY+disp) FD,CB,disp,16
RLA 17
RLC  reg CB,0 0rrr
RLC  (HL) CB,06
RLC  (IX+disp) DD,CB,disp,06
RLC  (IY+disp) FD,CB,disp,06
RLCA 07
RLD ED,6F
RR   reg CB,1 1rrr
RR   (HL) CB,1E
RR   (IX+disp) DD,CB,disp,1E
RR   (IY+disp) FD,CB,disp,1E
RRA 1F
RRC  reg CB,0 1rrr
RRC  (HL) CB,0E
RRC  (IX+disp) DD,CB,disp,0E
RRC  (IY+disp) FD,CB,disp,0E
RRCA 0F
RRD ED,67
RST  00 C7
RST  08 CF
RST  10 D7
RST  18 DF
RST  20 E7
RST  28 EF
RST  30 F7
RST  38 FF
SBC  A,reg 9 1rrr
SBC  A,data DE,yy
SBC  A,(HL) 9E
SBC  A,(IX+disp) DD,9E,disp
SBC  A,(IY+disp) FD,9E,disp
SBC  HL,rp ED,01xx 2
SCF 37
SET  b,reg CB,11bbbrrr
SET  b,(HL) CB,11bbb110
SET  b,(IX+disp) DD,CB,disp,11bbb110
SET  b,(IY+disp) FD,CB,disp,11bbb110
SLA  reg CB,2 0rrr
SLA  (HL) CB,26
SLA  (IX+disp) DD,CB,disp,26
SLA  (IY+disp) FD,CB,disp,26
SRA  reg CB,2 1rrr
SRA  (HL) CB,2E
SRA  (IX+disp) DD,CB,disp,2E
SRA  (IY+disp) FD,CB,disp,2E
SRL  reg CB,3 1rrr
SRL  (HL) CB,3E
SRL  (IX+disp) DD,CB,disp,3E
SRL  (IY+disp) FD,CB,disp,3E
SUB  reg 9 0rrr
SUB  data D6,yy
SUB  (HL) 96
SUB  (IX+disp) DD,96,disp
SUB  (IY+disp) FD,96,disp
XOR  reg A 1rrr
XOR  data EE,yy
XOR  (HL) AE
XOR  (IX+disp) DD,AE,disp
XOR  (IY+disp) FD,AE,disp

Les commandes cachées du Z80

Basé sur l'article publié dans Quasar CPC numéro 12, Assembleur : Software, par Zik.

Nous allons ici parler des commandes du Z80 qui sont mal connues ou plutôt “inconnues” ! En effet, elles ne sont mentionnées nulle part dans les documentations officielles sur le Z80 que j'ai eu et peu de désassembleurs les interprêtent toutes bien. Mon but est aussi de compléter le tableau ci-dessus.

La “découverte” intuitive de ces commandes provient d'une remarque sur les codes machines (que vous relèverez sur le tableau sus-mentionnée). Si vous regardez les codes des commandes concernant IX et IY par rapport aux équivalents sur HL, vous remarquerez que le code est le même avec comme préfixe l'octet &DD pour IX et &FD pour IY. Vite un exemple :

Je suis sûre que c'est caché quelque part par là !

  • INC HL est codé par &23
  • INC IX est codé par &DD,&23
  • INC IY aura pour code &FD, &23

Remarquable non ?! Bon soit… ce qui est intéressant c'est qu'il existe des commandes comme le LD H,12, RL H, etc. qui donc font des opérations 8 bits et les codes de ces commandes avec les préfixes 16) ne sont pas affectées à d'autres commandes. Et en fait, certaines de ces commandes non mentionnées marchent… mais pas toutes !

En clair, on peut grâce à ces commandes utiliser les registres IX et IY comme des paires de registres 8 bits un peu comme quand on scinde HL en H et L (traités séparément donc).

Voici l'additif au tableau précédent avec en plus le temps machine des commandes. J'ai pris comme convention d'écriture IXH pour l'octet de poids fort de IX et IXL pour le poids faible de IX. Le temps machine est exprimé en nombre de NOP équivalent.

2 ADC A,IXH DB &DD:ADC A,H
2 ADD A,IXL DB &DD:ADD A,H
4 ADD IX,IX DB &DD:ADD HL,HL
2 AND IXH DB &DD:AND H
2 CP IXH DB &DD:CP H
2 DEC IXH DB &DD:DEC H
2 INC IXH DB &DD:INC H
2 LD reg,IXH DB &DD:LD reg,H
2 LD IXH,reg DB &DD:LD H,reg
3 LD IXH,data DB &DD:LD H,data
2 OR IXH DB &DD:OR H
2 SBC A,IXH DB &DD:SBC A,H
2 SUB IXH DB &DD:SUB H
2 XOR IXH DB &DD:XOR H

La colonne de droite indique ce que vous devez taper dans votre source. Pour avoir les mêmes commandes en IXL il suffit de remplacer H par L, toujours dans la colonne de droite. Comme je l'ai déjà dit, pour utiliser IY à la place de IX, il faut changer le DB &DD en DB &FD.

Je vous ferai remarquer que ADD HL,IX et ADD IX,HL n'existent pas, par contre on a bien ADD IX,IX. J'espère que je n'ai pas oublié de commandes dans ce tableau, sinon je compte sur vous pour le corriger !

Au niveau désassemblage, le Hacker (du moins la version 4.81) connait toutes ces commandes, vous pouvez lui faire confiance à 100% ; The Insider comprend les LD (et encore, il fait quelques erreurs) ; DAMS n'a pas l'air d'y comprendre grand chose (il met des astérisques !) et Maxam ne connait pas (il met des points d'interrogation, chacun son style !). Sachez pour finir que vous pouvez parsemer vos sources de &DD ou &FD, quand ça ne forme pas les commandes ci-dessous le Z80 les ignore (moyennant 1µs).

La pile

Basé sur l'article publié dans Quasar CPC numéro 14, Assembleur : Software, par Zik.

Saviez-vous que le Z80 marche à pile ?! Très certainement puisque nous avons déjà parlé du registre SP précédemment. Il s'agit justement (quel hasard !) du pointeur de pile dit “Stack Pointer” (d'après mon dictionnaire ça se traduirait plutot par “index de tas”…).

À quoi sert-ce ?

Des piles d'assiettes... poussiereuse ! Tout d'abord, je vous remercie de me poser cette question. Une pile permet le stockage temporaire de données en mémoire de manière simple.

Son emploi consiste donc en fait en la sauvegarde d'un registre (16 bits sur Z80). C'est ainsi que PC (le Program Counter, qui contient à chaque instant l'adresse de l'instruction exécutée) est sauvée dans la pile au moment d'un CALL ou d'un RST pour permettre de retrouver la bonne adresse à la rencontre d'un RET.

Le détail !

Maintenant qu'on connait l'utilité de la pile, il est temps de s'intéresser à son fonctionnement (mais si !).

La pile du Z80 est de type LIFO (pour Last In, First Out). C'est-à-dire, en bon français, que le dernier éléments que vous y placez sera le premier à en sortir (sauf bidouille).

C'est là que je sors la bonne vieille comparaison avec une pile d'assiettes (j'ai dit “vieille” et pas “poussiéreuse”, la vaisselle est irréprochable chez moi). Donc, dans votre pile d'assiettes, celle qui se trouve en dessous est la première que vous avez empilée et, quand vous récupérez une assiette, vous prenez naturellement celle du dessus qui est bien la dernière mise dans la pile.

Voilà pour le principe, maintenant voyons plus concrètement comment cela est réalisé sur Z80…

  • Empilage : ceci peut être effectué par un PUSH
    • SP est décrémenté (d'un mot)
    • la valeur est stockée à l'adresse SP
  • Extraction : réalisée par un POP
    • lecture de la valeur à l'adresse SP
    • SP est incrémentée (d'un mot)

Les opérations sont décrites dans l'ordre chronologique?. Si leur ordre était inversé, la pile créée serait tout aussi fonctionnelle. M'enfin bon, Zilog a choisi la première méthode.

Bien sûr, les octets de poids faible et fort sont inversés lors de la lecture ou de l'écriture en mémoire par la pile. Voici maintenant un petit exemple pour résumer tout ça :

La trace du petit programme ci-dessous aboutirait aux informations en vis-à-vis (ainsi qu'à un superbe plantage de l'ordinateur…).

; Petit programme d'exemple
 
    LD SP,&C000
    LD BC,&BB5A
    LD DE,&568D
    LD HL,&ABCD
    PUSH DE ; (1)
    PUSH BC ; (2)
    POP HL  ; (3)

État de la mémoire

Adresse 1 2 3
&BFFF &56 &56 &56
&BFFE &8D &8D &8D
&BFFD &xx &BB &BB
&BFFC &xx &5A &5A

État des registres

Registre 1 2 3
SP &BFFE &BFFC &BFFE
BC &BB5A &BB5A &BB5A
DE &568D &568D &568D
HL &ABCD &ABCD &BB5A

Suivez les instructions

La liste des instructions ayant un rapport avec SP n'est pas très étendue mais quelques unes d'entre elles sont très puissantes et relativement rapides. C'est ainsi que l'on trouve des instructions de chargement registre 16 bits vers SP qui sont quand même remarquables sur un processeur 8 bits ! Voici donc cette liste, les chiffres indiquent le temps machine pris en nombre de NOP équivalent. Quand deux chiffres sont spécifiés ils correspondent au cas où la condition est vérifiée ou non.

ADC HL,SP ................ 4
ADD HL,SP ................ 3
ADD IX,SP ................ 4
ADD IY,SP ................ 4
CALL condition,adresse ... 5/3
DEC SP ................... 2
EX (SP),HL ............... 6
EX (SP),IX ............... 7
EX (SP),IY ............... 7
INC SP ................... 2
LD (adresse),SP .......... 6
LD SP,(adresse) .......... 6
LD SP,HL ................. 2
LD SP,IX ................. 3
LD SP,IY ................. 3
LD SP,adresse ............ 3
POP AF ................... 3
POP BC ................... 3
POP DE ................... 3
POP HL ................... 3
POP IX ................... 4
POP IY ................... 4
PUSH AF .................. 4
PUSH BC .................. 4
PUSH DE .................. 4
PUSH HL .................. 4
PUSH IX .................. 5
PUSH IY .................. 5
RET ...................... 3
RET condition ............ 4/2
RETI ..................... 4
RETN ..................... 4
RST adresse .............. 4
SBC HL,SP ................ 4

Comme vous pouvez le constater, l'intervention de la pile n'est pas toujours explicite. Concernant CALL et RET, la pile n'est modifiée que si la condition est vérifiée ; c'est-à-dire si le saut en mémoire est effectué.

La pile permet des transferts mémoire/registre très rapides, tout ça en décalant automatiquement le pointeur. D'où la tentation de détourner l'usage classique de la pile le temps d'exécuter notre routine adorée le plus rapidement possible.

La pile apprivoisée

Mais détourner la pile ne peut se faire qu'en prenant certaines précautions, sinon attention à la casse (cf. les assiettes de tout à l'heure).

Premièrement, il faut se méfier des interruptions qui modifient fatalement la pile. C'est pour cette raison que la plupart des routines qui détournent la pile interdisent les interruptions par un DI.

Ensuite, le fait d'utiliser la pile oblige souvent à se passer de CALL, RST et de la sauvegarde des registres par PUSH/POP.

Enfin, si vous espérez voir votre programme rendre la main au système sans protestation vive de ce dernier (plantages divers), n'oubliez pas de sauvegardez la pile en début de programme pour pouvoir la restituer en fin.

Si vous respectez ces quelques règles, il n'y aura en principe pas de problème. Donc, avec la pile vous ne perdrez pas la face (ça y est, je l'ai placée !).

Tout doux la pile de monstres !

Des exemples !

Premier exemple

Le premier programme est plutôt là pour montrer une technique car au niveau des performances il n'égale pas une classique routine d'affichage construite autour d'un LDIR. Cela malgré certains préjugés qu'on ne prend pas même la peine de vérifier (cf. ACPC 47 p.18 à 21) !

Bref, la pile est ici utilisée comme pointeur sur les données du sprite alors que HL adresse l'écran. Le POP récupère deux octets du sprite (avec une rapidité remarquable). Ensuite, on place ces deux octets à l'écran, c'est là que cette routine perd tout son intérêt en ce qui concerne son temps d'exécution.

Le reste n'est que calcul pour obtenir l'adresse écran de la ligne suivante (l'équivalent d'un CALL &BC26 en somme). Remarque bien qu'un CALL ne fonctionnerait pas (il déteriorerait les données du sprite).

; Affichage de sprite à la pile
 
        Org &8000
        Nolist ; pour Maxam
 
Ecran   Equ &c050
Sprite  Equ &a500
Hauteur Equ 50
Largeur Equ 18
; la largeur doit être paire
 
        di
        ld (save_sp+1),sp
 
        ld sp,sprite
        ld hl,ecran
        ld b,hauteur
Ligne   ld c,largeur/2
Colonne pop de
        ld (hl),e
        inc hl
        ld (hl),d
        inc hl
        dec c
        jr nz,colonne
        ld de,&800-largeur
        add hl,de
        jr nc,suite
        ld de,&c050
        add hl,de
Suite   djnz ligne
 
Save_sp ld sp,0
        ei
        ret

Deuxième exemple

Le deuxième exemple d'utilisation est un peu l'inverse du premier. SP pointe ici sur l'écran et HL contient ce qui doit être affiché (des 0 pour un effaçage !). Chaque PUSH place deux octets, pour un écran de 16384 octets on a donc besoin de 8192 PUSH. Or 8192=256*32 donc le compte est bon ! Le ”CLS” généré par cette routine est presque trois fois plus rapide que le meme fait par LDIR. Une routine similaire était parue dans un numéro d'Amstrad Cent Pour Cent, mais cela fait déjà 8 ans de ça ! Ben mon vieux !

; Effaçage d'écran à la pile
 
        Org &4000
        Nolist ; pour Maxam
 
        di
        ld (save_sp+1),sp
        ld sp,0
        ld hl,0
        ld b,0
Boucle  push hl:push hl ; y'en a 32
        push hl:push hl
        push hl:push hl
        push hl:push hl
        push hl:push hl
        push hl:push hl
        push hl:push hl
        push hl:push hl
        push hl:push hl
        push hl:push hl
        push hl:push hl
        push hl:push hl
        push hl:push hl
        push hl:push hl
        push hl:push hl
        push hl:push hl
        djnz boucle
 
Save_sp ld sp,0
        ei
        ret

Les instructions de contrôle et de test de bit

Basé sur l'article publié dans Quasar CPC numéro 17, Assembleur : Software, par Zik.

La diversité est source de richesse donc, après avoir vu toutes les petits choses qui précèdent, nous allons nous intéresser aux instructions de contrôle et de test de bit du Z80 !

Mode d'emploi...

Ça se lit comme un rien ! Les trois instructions en question sont BIT, SET et RES. Elles nécessitent deux arguments qui sont le numéro du bit concerné par l'opération et l'argument sur lequel elle a lieu. Celui-ci désigne forcément une valeur 8 bits contenue soit dans un registre 8 bits soit en mémoire à l'adresse pointée par HL ou par un des registres d'index (IX ou IY). Le numéro de bit spécifié est donc un nombre de 0 à 7. Le zéro étant le poids faible comme d'habitude.

Les instructions RES et SET permettent de mettre le bit souhaité respectivement à 0 ou à 1. Voici un petit exemple : si HL contient &1234 et que la mémoire à l'adresse &1234 vaut &1B, alors après l'exécution de SET 6,(HL), la valeur en mémoire en &1234 est &5B et aucun registre n'a été modifié.

Par ailleurs, ces deux instructions ne modifier aucun flag contrairement aux opérations logiques (AND, OR et XOR) qui peuvent elles aussi forcer des valeurs de bits (mais seulement sur A).

L'instruction BIT permet de tester l'état d'un bit donné d'un registre (8 bits) ou en mémoire. Le résultat du test est donné par le flag Z qui est mis si le bit considéré vaut 0. Mais BIT modifie en fait tous les flags sauf la Carry comme suit :

Non mais vous n'avez pas fini de regarder sous les couches !

Flag État après l'instruction BIT Description
Sindéterminé flag de signe
Zmis à 1 si le bit spécifié vaut 0, mis à 0 sinon flag de zéro
Hmis à un 1 flag de demi-Carry
P/Vindéterminé flag de parité/overflow
Nmis à 0 flag Add/Substract
Cinchangé flag de Carry

Les flags Add/Substract et de demi-Carry ne sont pas consultables directement, il faut aller voir dans le registre F qui est consitué de la manière suivante :

 7   6   5   4   3   2   1   0 
S Z X H X P/V N C

Je ne donnerai pas ici plus de détails sur les flags. Finissons plutôt par une illustration de l'instruction BIT :

        ld ix,&1000
        ld (ix-16),&3f
        bit 4,(ix-16)
        jp nz,toujours
Jamais  ...

Codage des instructions

Instruction n,r :

1 1 0 0 1 0 1 1 &CB
←i→ ←n→ ←r→ iinnnrrr

Instruction n,(IX+d) :

1 1 0 1 1 0 0 1 &DD
1 1 0 0 1 0 1 1 &CB
←d→ dddddddd
←i→ ←n→ ←r→ iinnnrrr

Instruction n,(IY+d) :

1 1 1 1 1 0 0 1 &FD
1 1 0 0 1 0 1 1 &CB
←d→ dddddddd
←i→ ←n→ ←r→ iinnnrrr

Où :

Instruction i
BIT 01
RES 10
SET 11
Bit n
0 000
1 001
2 010
3 011
4 100
5 101
6 110
7 111
Registre r
B 000
C 001
D 010
E 011
H 100
L 101
(HL) 110
A 111

 

   Un petit SET, un petit BIT, un petit RES... on secoue... et voilà !

Temps machine

Pour :

BIT n,argument
RES n,argument
SET n,argument

On a :

argument   BIT   RES/SET
reg 8 bits 2µs 2µs
(HL) 3µs 4µs
(IX+d) 6µs 7µs
(IY+d) 6µs 7µs

Où : reg 8 bits = A, B, C, D, E, H, L

Documentations externes

1) Comment ça vous n'avez rien compris ?
2) Ils sont 8 bits.
3) Ils sont 16 bits.
4) base 16
5) &FF=255
6) ou routines système
7) ou DEFB avec les vieux assembleurs
8) ou DEFW avec les vieux assembleurs
9) ou littéralement des drapeaux
10) en pratique, 255=-1… mais nous reparlerons plus tard des nombres signés en assembleur
11) en fait, deux registres 8 bits appairés
12) source
13) destination
14) registre
15) bit
16) &DD' et &FD
 
iassem/z80init.txt · Dernière modification: 2017/10/09 10:03 (édition externe)