Assembleur Z80 sur RC2014 - épisode 2 - l'adressage

Bonsoir à tous,

Voici la suite des découvertes de l'assembleur Z80 sur RC2014 en utilisant SCM (Small Computer Monitor, voir sa configuration), il est possible de faire de l'assembleur directement sur le RC2014-Pro.

Nous continuons le visionnage des vidéos de Matt Heffernan relatif à la programmation Z80 sur Spectrum pour adapter les exemples au RC2014.


Episode 2: l'adressage

Le processeur Z80 n'est pas capable de traiter et modifier les informations directement dans la RAM. Par conséquent, Le Z80 utilise des registres dans lesquels il charge l'information pour la modifier avant de l'écrire à nouveau dans l'espace RAM.

Le chargement de donnée depuis la RAM dit "Load" en anglais (d'où l'opcode LD) est donc essentielle au fonctionnement des programmes. Cette instruction LD est permet de réaliser une assez grande variété de transferts différents (voir mode d'adressage).

Cette seconde vidéo de Matt Heffenan traîte des différents modes d'adressage du Z80.

La mémoire sur le RC2014

Comme nous allons parler de transfert de données entre la mémoire RAM (ou ROM lors du Fetch d'instruction) et la mémoire du processeur (les registres).

C'est donc l'occasion idéale de revenir sur l'organisation mémoire du RC2014 avec la ROM88 (SCM, Basic, CP/M loader).

Espace d'adressage du RC2014

Espace d'adressage RC2014

La RAM est disponible à partir de l'adresse $8000. C'est à partir de cet adresse qu'il est possible de stocker du contenu.
Le Small Computer Monitor (SCM) est exécuté depuis la ROM (0x0000 à 0x3FFF). La ROM étant en lecture seule, il n'est pas possible d'en modifier le moindre octet.

J'utilise SCM pour assembler mes programmes à partir de $8000 et je stocke les données à partir de $8500.

Les registres du Z80

Il peut toujours être utile de rappeler la structure des registres du processeur.


L'accumulateur (A) intervient dans de nombreuses opérations arithmétique et permet de charger un octet depuis la mémoire RAM.

Par exemple: il n'est pas possible de charger une donnée de la RAM directement dans le registre B, il faut charger la donnée dans A puis transférer le registre A vers B.

A contrario, il est tout à fait possible de charger un mot (2 octets) directement dans les registres BC une donnée 16 bits en provenance de la RAM. C'est pareil pour DE et HL, Il peuvent charger des données 16 bits avec une instruction LD LH,($8005)... mais pas question de faire un LD H,($8005) pour charger uniquement 8 bits.

Une petite note sur les registres IX et IY qui servent d'indexes de référence pour charger de données depuis la mémoire en utilisant un offset par rapport à l'index mentionner ou en incrémentant la valeur d'index.

Les modes d'adressage Z80

S'il n'est pas utile de savoir identifier chacun des modes d'adressage, il est par contre utile de savoir qu'ils existent de pouvoir les exploiter.

S'il y a un tel détails dans les modes d'exécution c'est parce que cela implique parfois un nombre de cycles d'exécutions plus ou moins longs selon le mode utilité.

Voici les modes détaillés dans la vidéo:

  • Mode d'adressage implicite - Implied Adress Mode
  • Adressage de registres - Register adressing
  • Adressage immédiat (8 bits) - Immediate addressing
  • Adressage immédiat étendu (16 bit) - Extended immediate addressing
  • Adressage indirect - indirect addressing
  • modes d'adressage complémentaires abordés plus bas

Mode d'adresse implicite - Implied address mode

Il s'agit des opcodes (code opération) tenant sur un seul octet qui modifient la mémoire du microcontrôleur (les registres) durant leur exécution. Ces opcodes n'ont, par conséquent, pas d'opérande.

Ex:

  • nop
  • di
  • ret
  • rla
  • halt
  • ei
  • exx
  • ldir

Adressage de registre - register adressing

Lecture ou écriture d'une valeur d'un registre vers un autre. Ces opérations son codées sur un opcode d'un seul octet sans aucune opérande.

Donc pas de fetch complémentaire en mémoire ROM pour lire ces instructions, ni d'accès RAM.

L'exécution de ces instructions est extrêmement rapide.

  • ld a,i
  • ld b,c
  • ld h,l
  • ld r,a
  • ld ixh, ixl ; 8 bits
  • ld iyl, iyh
  • ld sp,ix ; 16 bits
  • ld sp,iy ; 16 bits
  • ex af,af' ; échange des registres
  • push bc ; 16 bits
  • pop de
  • in f,(c)

Adressage immédiat - immediate addressing

Ces opérations sont utilisées pour transférer une valeur 8bits (0 à 255, $00 à $FF) directement dans un registre.

L'opcode est accompagné d'un octet supplémentaire qui contient l'opérande (la valeur à charger). Ces opcode réclament le fetch d'un octet supplémentaire de la ROM (après le fetch de l'opcode).

Il n'y a pas de fetch supplémentaire en RAM après la capture de opérande en ROM.

  • ld a,42 ; decimal
  • ld b, %10001000 ; binaire
  • ld c, $2A ; hexadécimal
  • cp 123

Adressage immédiat étendu - extended immediate addressing

C'est comme l'adressage immédiat sauf que c'est 2 octets qui sont chargés depuis la ROM pour constitué une valeur 16 bits. Il n'y a pas d'accès en RAM après le fetch des deux octets complémentaires en ROM.
 
Avec 16 bits, il est possible de coder un valeur entre 0 et 65535 (ou -32768 à 32767 en utilisant un complément à deux).
La valeur doit absolument être transférer dans un registre 16bits (donc combinaisons bc, de, hl).
 
La gamme de valeur en hexadécimal va de $0000 à $FFFF. A noter que dans la ROM, l'information est stockée en Little Endian (octet de poids faible en premier).
  • ld bc, 4660 ; decimal
  • ld de, %1000100010001000 ; binaire
  • ld hl, $FFFF ; hexadecimal
  • jp $89AB ; saut direct

Adressage indirect - indirect addressing

L'adressage indirecte est une instruction qui permet de lire ou écrire une valeur depuis la mémoire RAM.
En fonction de l'opcode et de l'opérande, l'adresse mémoire peut être encodée sur 16 bits (ou 8 bits).
L'adressage direct est reconnaissable à l'aide des parenthèses entourant l'adresse d'où il faut charger la donnée.
A noter que seul le registre A permet de faire un adressage 8 bits,
  • ld a,($8500) ; lecture de 8bits depuis l'adresse $8500
  • ld ($8505),a ; écriture du registre A à l'adresse $8505
  • ld de, ($8600) ; lecture de 16bits depuis la mémoire. Ecriture dans les registres D & E.
  • ld ($8602), hl ; écriture du contenu des registres H & L (16 bits) à l'adresse mémoire $8602 (et $8603)
  • ld de, (hl) ; lecture de 16 bits à l'adresse mentionnée dans le registre HL.

Autres modes d'adressage

Il existe encore quelques autres modes d'adressages qu'il faut connaître:
  • Adressage en page zero - Zero page addressing
  • Adressage relatif - Relative addressing
  • Adressage par index - Index adressing
  • Adressage de bits - Bit addressing
  • Exécution sur flag - execution on specific status flag
  • Mode d'interruption - interrupt mode

Adressage en page zero - Zero page addressing

Référence la première page mémoire de $0000 à $00FF.
Les instructions RS $00 à RST $FF sont codés sur un octet (donc extrêmement rapide en terme d'exécution) et permettent de faire des saut spécifiques depuis la page zero vers des routines spécifiques.

Dans le monde Z80, cela s'appelle les Vecteurs Reset (Reset Vector).

Dans le tuto sur le ZX Spectrum mentionne les Vecteurs Reset suivant:

  • RST $00, RST $08, RST $10, RST $18
  • RST $20, RST $28, RST $30, RST $38

Sur le RC2014, j'ai déjà identifié le vecteur suivant:

  • RST $30 : appel d'API RC2014 offrant différents services comme l'affichage/saisie de caractère sur la console.

Adressage relatif - Relative addressing

L'adressage relatif permet de faire un saut lorsque l'offset d'adresse tient dans 8 bits (-128 à +127). Cela évite de devoir préciser une adresse complète sur 16 bits. Instruction plus courte = exécution plus rapide.

  • jr nz,$28 ; jump relative if not Zero flag
  • djnz $20
  • jr z,$30 ; jump relative if Zero flag

Adressage par index - Index adressing

Utilise les registre d'adresse IX et IY et une offset de -128 à +127 pour effectuer une lecture en RAM.

  • ld a,(IX+11)
  • ld (IY-103),B
Remarque: Je ne suis pas arrivé à assembler ce type d'instruction avec offset sur SCM.

Adressage de bits - Bit addressing

Instruction permettant de tester ou modifier un bit de 0 à 7 dans un registre ou un octet en RAM.

  • bit 1, a ; teste 2ieme bit du registre A
  • set 3,(hl) ; mettre 4ieme bit a 1 à l'octet situe en RAM à l'adresse HL
  • res 6,(IX+42) ; reset du 7ieme bit

Exécution sur flag - execution on specific status flag

permet d'exécuter une instruction de saut sur base d'une condition.

  • jp c,$8899 ; jump if carry bit is set (C=1)
  • ret nc ; return is no carry bit (C=0)
  • call z,$8000 ; call if zero is set (Z=1)
  • jp nz,$8010 ; call if non zero (Z=0)
  • ret m ; return is sign bit set (S=1)
  • call p,$9099 ; call if sign bit reset (S=0)
  • jp pe,$8500 ; jump if overflow bit set (p/v=1)
  • jp po,$8600 ; jump is overflow bit reset (p/v=0)

Mode d'interruption - interrupt mode

IM 0, IM 1, IM2 sont des instructions en un octet chacun. Cette instruction permet de modifier la façon dont le Z80 réagit à une interruption.

Noter: Small Computer Monitor (SCM) sur le RC2014 n'utilise pas d'interruption. Ce mécanisme reste donc disponible pour vos propres programmes.

  • IM 0 ; spécifique aux périphériques (non exploiter sur Spectrum).
  • IM 1 : appel le gestionnaire d'interruption en ROM à l'adresse $0038.
  • IM 2 : utilise un "table vector" (Page=I) pour faire le saut, ce qui permet de définir le gestionnaire d'interruption dans le programme.

Tester l'adressage indirect

Dans l'exemple ci dessous, nous allons tester l'adressage indirect sur notre RC2014 avec Small Computer Monitor (SCM).

Qu'allons nous faire?

Nous allons préparer des données en RAM à l'adresse $8500.

Ensuite nous allons lire l'octet à l'adresse $8501 en utilisant l'adressage indirecte. Pour cela la valeur $8501 est stockée dans le registre HL puis ce dernier registre est utilisé pour charger la donnée dans l'accumulteur (registre A).

Nous vérifierons ensuite que la valeur attendue est bien présente dans l'accumulateur.

Etape 1: Préparer les données

Utiliser la commande "e 8500" pour y stocker une chaîne de caractère et quelques octets $00 .

*e 8500
8500: 5A           Z     LD E,D                > "Z80 SCM is awesom
8511: 6F           o     LD L,A                > 00
8512: 6D           m     LD L,L                > 00
8513: 65           e     LD H,L                > 00
8514: 00           .     NOP                   > 00
8515: 00           .     NOP                   > 00
8516: 00           .     NOP                   > .
*

Ensuite on vérifie le contenu de la RAM avec le commande "m 8500".

*m 8500
8500:  5A 38 30 20 53 43 4D 20  69 73 20 61 77 65 73 6F  Z80 SCM is aweso
8510:  6D 00 00 00 00 00 00 00  94 AD C1 E3 53 1D E5 6A  m...........S..j
8520:  B5 C3 71 BA 6E DC 62 5E  81 FC B6 BC D8 11 07 E5  ..q.n.b^........
8530:  D1 D0 22 C2 01 EC 21 18  6C 36 50 BA 44 5A 69 69  .."...!.l6P.DZii
8540:  01 B1 35 A9 B3 BE F4 D2  52 0F 64 52 E3 3B 9C 91  ..5.....R.dR.;..
8550:  61 77 7E 99 DB 8A 9E 91  74 76 78 11 B3 26 46 C3  aw~.....tvx..&F.
8560:  D5 57 1B 5C F1 C3 79 3E  14 38 F3 6D F6 EA 23 3D  .W.\..y>.8.m..#=
8570:  41 33 79 0C 50 82 3C 4B  B0 9A CC 12 80 7C 10 5A  A3y.P.<K.....|.Z

On constate que l'octet à l'adresse $8501 est $38 (soit 56 en décimal), ce qui correspond au caractère ASCII "8".
Les $00 encodés permettent de séparer nos données du contenu aléatoire de la RAM. C'est plus confortable à la lecture.

Etape 2: Effacer la mémoire

Cette fois, nous allons effacer la mémoire en $8000 avec la commande "fill" avant d'y assembler notre programme.
Ensuite, désassembler la mémoire en $8000 présente une série d'instruction NOP (no operation).

*fill 8000 8100 00
*
*d 8000
8000: 00           .     NOP
8001: 00           .     NOP
8002: 00           .     NOP
8003: 00           .     NOP
8004: 00           .     NOP
8005: 00           .     NOP
8006: 00           .     NOP
8007: 00           .     NOP
8008: 00           .     NOP
8009: 00           .     NOP
800A: 00           .     NOP
800B: 00           .     NOP
800C: 00           .     NOP

Etape 3: Saisir le code

Nous allons assembler le code suivant dans SCM

ld hl,$8501 ; Memoire à lire
ld a,(hl)   ; charger valeur de $8501 dans accumulateur
ret         ; retour au moniteur SCM

L'assemblage commence avec l'instruction "a 8000". L'assemblage es terminé avec un point "." .

*a 8000
8000: 00           .     NOP                   > ld hl,$8501
8000: 21 01 85     !..   LD HL,$8501
8003: 00           .     NOP                   > ld a,(hl)
8003: 7E           ~     LD A,(HL)
8004: 00           .     NOP                   > ret
8004: C9           .     RET
8005: 00           .     NOP                   > .

L'instruction RET est codée en $8004

Etape 4: contrôle des registres

Avant de démarrer le programme, nous allons utiliser l'instruction "r" pour inspecter les registres.

*r
PC:8004 AF:614E BC:7BAE DE:6D84 HL:8500 IX:EB4A IY:EE39 Flags:-Z---PN-
SP:FCBE AF'78A2 BC'423D DE'FCF6 HL'0C13 (S)0003 IR:D209 Flags'S-----N-
*

On peut y voir que le registre "a" contient actuellement la valeur $61. Nous pourrons donc constater qu'elle à bien changée.

Etape 5: placer un point d'arrêt

De très nombreuses instructions modifient le contenu des registres. Il faut donc contrôler le contenu du registre A juste après l'exécution de "ld a,(hl)" et avant l'instruction "ret".

C'est là qu'intervient le point d'arrêt (dit Breakpoint en anglais) puisqu'il permet de s'arrêter avant l'exécution du RET.

*b 8004
Breakpoint set
*

Etape 6: exécution et contrôle

Nous allons maintenant démarrer le programme (g 8000) qui s'arrêtera en $8004 (juste avant le RET).

*g 8000
Breakpoint
PC:8004 AF:384E BC:7BAE DE:6D84 HL:8501 IX:EB4A IY:EE39 Flags:-Z---PN-
*r
PC:8004 AF:384E BC:7BAE DE:6D84 HL:8501 IX:EB4A IY:EE39 Flags:-Z---PN-
SP:FCBE AF'78A2 BC'423D DE'FCF6 HL'0C13 (S)0003 IR:D235 Flags'S-----N-
*

Dès le breakpoint activé, le moniteur affiche directement les registres principaux sont affichés.

En saisissant la commande "R", il sera possible de voir le contenu détaillé des registres.

Il est possible de constater la valeur $38 dans le registre A... comme cela est attendu (la lettre "8" du message "Z80 is awesom").

Note: l'assembleur aurait pu être réduit à une ligne ld a,($8501)

C'est pas fini!

La vidéo présente l'affichage d'une chaîne de caractères. Le présent article est bien assez long, je vais donc reporter l'écriture de ce programme assembleur aux deux articles suivants...  parce qu'il faudra aussi parler d'appel d'API RC2014 (nous ne sommes pas sur un Spectrum!).

Aucun commentaire