L'assembleur Z80 pour les nuls

(Un tutoriel d'Alexis Guinamard, écrit en 2002/2003.)

Ce tutoriel est destiné à tous ceux qui veulent apprendre l'asm Z80 pour les 83 et 83+ (ION), et qui n'ont pas trouvé leur bonheur avec ASM Guru par exemple... Je ne vais pas polémiquer, mais je trouve que la plupart des tutoriels sont mal faits car ils apprennent les rom calls de TI, mais finalement, pas vraiment la logique de l'asm... Je ne vais pas là tout enseigner, juste les bases qui vont vous permettre de faire des programmes corrects et d'apprendre plus ou moins seuls après...

De plus, la plupart d'entre vous devez connaitre le TI-BASIC... Oubliez TOUT ce que vous savez à propos de ce langage, l'ASM n'a quasiment rien avoir avec et chercher des correspondances entre basic et asm ne fait que ralentir l'apprentissage de l'assembleur... Enfin, j'ai choisi de ne parler que de programmes pour ion, pour une meilleure compatibilité entre la 83 et la 83+. Cependant, le code en asm reste identique sur d'autres plateformes utilisant le Z80. Commençons!

Pour commencer, vous devez disposer des fichiers nécessaires à la compilation (ion.inc, tasm, devpac83, asm.bat , etc) Vous pouvez les trouver à l'adresse http://tift.free.fr/asm.zip. Il vous faut également impérativement une liste des instructions du z80, vous pourrez ainsi voir ce que vous pouvez faire, et ce que vous ne pouvez pas faire... J'en ai mis une à la fin de ce tutoriel.

Ce n'est pas vraiment un chapitre du tutoriel, mais il faut le dire, donc, voilà le 'header' de ion, ce sont des informations à insérer à chaque début de programme, elles sont utiles pour le compilateur (TASM) et pour le shell ION... Sinon, tout ce qui est précédé d'un ';' est considéré par TASM comme un commentaire... N'hésitez pas à en mettre beaucoup pour vous relire sans trop de problèmes...

        .nolist
#include "ion.inc"
    .list
#ifdef TI83P
    .org progstart-2
    .db $BB,$6D
#else
    .org progstart
#endif
    ret
    jr nc,start
    .db "nom_du_programme",0
start:
;programme...
.end ; c'est ici la fin du fichier

Note: Les instructions doivent toujours être précédées d'une tabulation, et par soucis de clarté, il est recommandé de mettre une autre tabulation entre l'instruction et ses paramètres:

ld    a,b

sera préféré à

ld a,b

De plus, pour quitter un programme, on utilise l'instruction 'ret' (RETurn)

Nous allons maintenant passer au chapitres:

Les registres

Les registres sont pour ainsi dire la base de l'assembleur, il est impossible de faire un programme en asm sans passer par des registres. En fait, ce sont des variables temporaires, qui servent à faire les calculs, à faire des tests, etc... Il existe 2 types de registres: les registres 8bits: a,b,c,d,e,h,l , et les registres 16 bits : bc,de,hl. Ces derniers sont en fait une 'paire' de registres 8 bits: bc est en fait une paire constituée de b et c.  Les registres 8 bits peuvent contenir des nombres entiers compris entre 0 et 255 (2^8 possibilités) Les registres 16 bits peuvent eux contenir des valeurs entre 0 et 65535 (2^16). Il existe d'autres registres, mais qui sont plus particuliers, et que nous verrons plus tard...

Charger une valeur dans un registre:

l'instruction 'ld' permet de charger une valeur dans un registre. Cette valeur peut venir d'un autre registre, d'une adresse dans la mémoire ou encore directement une valeur:

Pour les registres 8 bits:

ld     a,21    ;charge dans a la valeur 21
ld     b,a     ;charge la valeur de a dans b
ld     c,35    ;charge la valeur 35 dans c
ld     h,c     ; " " " de c dans h

Pour les registres 16 bits:

ld     hl,569   ;charge la valeur 569 dans hl

Note: Il n'existe pas d'instruction pour charger un registre 16 bits dans un autre, on utilise deux instructions pour cela, si on veut mettre la valeur de hl dans de, on fera:

ld     d,h
ld     e,l

Ici, on charge le premier registre de hl soit h dans le premier registre de de soit d , puis on refait pareil pour les seconds.

Remarque: si on veut mettre la valeur de a dans hl, on fera:

ld     h,0
ld     l,a

Calculs de base avec les registres

La base de tout programme est bien sûr les calculs. On peut faire les calculs simples assez facilement:

Incrémentation:

On peut incrémenter n'importe quel registre très facilement, avec l'instruction inc

inc     a     ;incrémente a (a=a+1)
inc     c     ;c=c+1

inc     hl    ;incrémente hl
inc     de    ;incrémente de

Décrémentation:

Comme pour incrémenter, on peut décrémenter n'importe quel registre avec l'instruction dec

dec     a
dec     hl

L'addition:

On peut additionner deux registres entre eux avec l'instruction add, mais pas n'importe quels registres, le premier sera toujours le registre a ou le registre hl, le second, celui qu'on veut, mais attention, on ne peut faire des calculs qu'entre registres 8 bits ou entre registres 16 bits... Suivant les cas, le résultat de l'addition est contenu dans 
a ou dans hl

Pour les registres 8 bits:

add     a,6     ;additionne a et 6
add     a,b     ;additionne a et b. Le résultat est dans a

Pour les registres 16 bits:

add     hl,de   ;additionne hl et de. Le résultat est dans hl

il n'existe pas d'instruction pour additionner hl et une valeur directement... Pour additionner hl à une valeur, on fera donc:

ld     de,45
add    hl,de 

La soustraction:

Pour les registres 8 bits:

On peut soustraire deux registres 8 bits avec l'instruction sub, comme pour l'addition, l'opération effectuée est toujours a=a-un autre registre ou a=a-une valeur. La syntaxe est juste un peu différente:

sub     b     ;a=a-b
sub     5     ;a=a-5

Ici, on ne mentionne pas le registre a dans l'instruction, contrairement à l'instruction add.

Pour des registres 16 bits:

On utilise ici l'instruction sbc qui permet de soustraire hl à un autre registre:

sbc     hl,de  ;hl=hl-de

Multiplication et division:

Notre petit Z80 n'a malheureusement pas réellement d'instructions pour multiplier ou pour diviser, il faut alors faire ses propres routines en combinant des add et des sub principallement, mais aussi avec les instructions sla et sra qui respectivement multiplient ou divisent un registre 8 bit par 2. Par exemple, pour multiplier a par 9, on pourra faire :

ld     b,a     ;sauvegarde la valeur de a dans b
add    a,a     ;\
add    a,a     ; | multiplie a par 8, c'est pareil que sla a
add    a,a     ;/
add    a,b     ; additionne a et b => a est multiplié par 9

Pour diviser a par 16 on peut faire :

sra    a       ;\
sra    a       ; | a/2/2/2/2 = a/16
sra    a       ; |
sra    a       ;/


Les multiplications et divisions par un registre sont plus compliquées, et demandent des instructions que nous n'avons pas encore étudié. On les verra donc plus tard.

Les variables

Les registres, nous l'avons vu, sont en quelque sorte des variables temporaires, mais on a bien souvent besoin de sauvegarder des valeurs durablement. Pour cela, on peut utiliser des variables, stockées dans la ram de notre 83+. Mais où ?

En fait, il existe des zones de mémoire libre dans la ram des calculatrices, ION nous permet de ne pas avoir à manipuler directement ces zones avec leurs adresses mémoire... Il existe 3 zones: saferam1 saferam2 et saferam3... saferam1 est la plus régulièrement utilisée et est une zone de 768 octets... Pour définir ses variables, rien de très compliqué, il suffit au tout début du programme de mettre:

#define nomdevar1 saferam1
#define nomdevar2 nomdevar1+1
#define nomdevar3 nomdevar2+2

...

Une ligne #define alias adresse signifie que plus tard dans le code, toute occurence de alias est remplacée par adresse au moment de la compilation. De même, saferam1 est lui-même un alias pour une adresse mémoire définie dans le fichier d'en-tête "ion.inc" déclaré au début du code. Comme dans l'exemple ci-dessus, on peut ajouter "+1", "+2, "+42"... à la partie droite du #define: cela signifie que l'alias défini pointe 1, 2 ou 42 octets après l'adresse spécifiée. C'est pourquoi on voit souvent au début des programmes une succession de define avec des alias suivis de "+1" ou "+2", car cela permet de ranger des valeurs de 8 bits ou de 16 bits.

On a maintenant défini nos variables, il faut les utiliser! On utilise pour cela l'instruction ld pour charger une variable dans un registre. Par exemple:

ld     a,(nomdevar1)

cette instruction charge dans a la valeur contenue à l'adresse nomdevar1 autrement dit, la valeur de cette variable.

ld     (nomdevar1),a

Cette instruction charge à l'adresse nomdevar1 la valeur de a autrment dit charge dans notre variable la valeur de a...

NOTE: On ne peut charger une variable 8bits que dans a, et inversement, on ne peut que charger a dedans.

ld     hl,(nomdevar2)

ici, on charge dans hl la valeur de la variable nomdevar2...

ld     (nomdevar2),hl

Vous ne devriez pas avoir trop de mal à comprendre ceci... :)

NOTE: Contrairement aux variables 8 bits, les variables 16 bits peuvent être chargées dans hl, bc ou encore de , et inversement... 

Les sauts et les sauts conditionnels

Parfait, on sait maintenant calculer avec les registres, mais à quoi ça sert si on ne sait pas utiliser des sauts conditionnels, les labels, les fonctions ? Dans un bon programme, tout le code n'est pas en un bloc, il existe des sous fonctions... Elles sont représentées par des labels

label1:
;code contenu dans la sous-fonction


Pour sauter à un label, sans condition particulière, on utilise l'instruction jp ou l'instruction jr:

jp     label1 ;saute à la sous fonction 'label1'
jr     label1 ; "         "       "         "


La différence entre les deux est que jp est plus rapide que jr, mais prend un octet de plus. De plus, jr ne peut sauter qu'à un label proche (128 octets avant ou 127 octets après) alors que jp peut appeller une fonction n'importe ou dans le programme...

Les sauts conditionnels:

Quand on veut sauter à un label dans une condition particulière, on va utiliser deux instructions: cp et jp L'instruction cp permet de tester la différence entre le registre a et un autre registre ou une valeur, en fait, cette instruction fait a-registre ou valeur, sans changer la valeur de a comme le fait l'instruction sub. Le résultat de l'instruction cp est qu'un registre particulier est mis à jour: le flag (registre f)

Le flag est un registre 8 bits qui contient plusieurs informations. Ces informations permettent de savoir si le résultat du dernier calcul (n'importe quel calcul excepté les inc et dec sur des registres 16 bits, et add sur des registres 16 bits) est positif, négatif, nul, pair, impair... On va donc pouvoir utiliser l'instruction jp pour sauter à notre label, de la manière suivante

Après l'instruction cp b par exemple, on fera:

jp     z,label1 si le dernier résultat était 0 c'est à dire ici si a=b
ou
jp     nz,label1 "       "        "    différent de 0 cad ici si a!=b
ou
jp     p,...     "       "        "    >0 c a d si a>b
ou
jp     m,...     "       "        "    <0 c a d si a<b



L'instruction cp permet aussi de comparer a et une valeur:
cp     5 ;compare a et 5 par exemple

Pour les sauts conditionnels, on utilisera de la même manière les z , nz , p , m ... Cela marche égallement avec jr.

Lors d'un saut conditionnel, le saut sera fait que si la condition est vérifiée, sinon, le programme continue à la suite....

Il existe une autre instruction très intéressante quand on veut faire un saut conditionnel: l'instruction call. la syntaxe est la même que pour jp et jr. La différence, est que une fonction appellée doit se terminer par un ret, qui permet de retourner exactement à l'endroit ou la fonction a été appelée, c'est très pratique...

exemple:

ld     a,5
cp     4
call   p,fonction    ;ici 5>4 donc le programme va aller dans la fonction 'fonction'
;suite du programme
;....

fonction:
;code de la fonction
ret                  ;ici, le ret permet de retourner là ou j'ai mis "suite du programme"


L'instruction call est très utile quand on a par exemple une routine que l'on doit appeler de différents endroits de notre programme.

La pile

La pile est un elément extrêmement important de tout assembleur. C'est en fait une structure dans la quelle on fait rentrer et sortir des valeurs 16 bits suivant la norme LIFO : Last In First Out (contrairement à la norme FIFO : first in first out). C'est donc en français la dernière valeur que l'on a rentré dans la pile qui en ressortira en premier. Plus concrètement, on peut imaginer une pile de papiers avec une valeur sur chacun, on peut récupérer le papier en haut de la pile ou en poser un autre sur le dessus. On y fait rentrer n'importe quel registre 16 bits (af, bc, de, hl ) avec l'instruction 'push'. On fera sortir une valeur avec l'instruction 'pop' et dans n'importe quel registre 16 bits (comme pour push). Concrètement, la pile sert à deux choses différentes : La sauvegarde de valeurs : On se sert de la pile pour sauver plus ou moins temporairement des valeurs. En effet, c'est plus rapide que de sauvegarder les valeurs dans des variables.

push hl ; met la valeur de hl dans la pile

pop de ; sort la dernière valeur de la pile et la met dans de

    La sauvegarde de l'adresse de retour après un call : Nous avons vu que quand on fait un call le ret nous fait retourner à l'adresse de départ. C'est du au fait que le call saute à l'adresse de la routine et sauvegarde l'adresse où ce call est placé dans la pile. Ensuite, le ret prend la valeur qui se trouve dans la pile et saute à cette adresse. Quand on quitte un programme, le ret fait donc retourner à la dernière valeur dans la pile qui est l'adresse du shell. 

    C'est pourquoi quand on se sert de la pile, il est important de savoir ce que l'on fait pour éviter des gros plantages : dans toute routine appellée par un call, il ne faut pas récuperer un valeur qui était dans la pile avant le call directement : il faut toujours autant de push que de pop dans une même routine, et à la fin d'in programme, s'assurer que toutes les valeurs qu'on a rentré dans la pile au cours du programme sont bien sorties. Je recommande aux tout débutants de ne pas trop s'aventurer dans la pile...

Quelques ROM Calls

    Nous avons vu comment faire les calculs de base avec les registres, comment les enregistrer dans la pile ou dans des variables, et les sauts conditionnels, mais nous n'avons aucune idée de comment afficher du texte ou tracer une ligne à l'écran... Pour cela, on utilise des rom calls. Ce sont en fait des routines que Ti a programmé pour son Ti-OS. Elles sont plus ou moins rapides mais permettent de commencer à faire des programmes dignes de ce nom. Comme je l'ai dejà dit, ce n'est pas le but de ce tutoriel de voir toutes les rom calls, nous allons voir les plus importantes. Certaines nécessitent de mettre une valeur particulière dans des registres ou variables, d'autres non.

On appelle une rom call par l'instruction bcall(_nom de la rom call)

ROM Calls générales:

clrlcdf ou clrlcdfull (clear lcd full) : efface tout l'écran. A mettre toujours en début de tout programme !
On fera donc ici :

bcall(_clrlcdf)

cphlde (compare hl et de): il n'existe pas d'instruction pour comparer 2 registres 16 bits. Cette rom call la remplace. Le saut conditionnel sera ici comme avec un 'cp' normal. L'opération testée étant ici hl-de.

getkey: Voilà une rom call très intéressante, pour scanner le clavier, et récupérer la valeur de la touche pressée. Cette rom call est à utiliser dans vos tests de programmes mais pas dans les programmes définitifs car [2nde]+[quit] fait revenir directement au tios sans quitter le programme... Elle est très pratique quand même, on peut également s'en servir pour faire une pause, juste pour attendre la pression d'une touche. Elle retourne dans a la valeur de la touche pressée.

bcall(_getkey)
cp     1 ;teste si c'est la touche [<-] qui a été pressée.


Voila les valeurs des principales touches:

[bas]       $4
[haut]      $3
[<-]         $1 
[->]         $2 
[enter]     $5 
[clear]     $9
[mode]    $45
[del]        $A
[graph]    $44
[trace]     $5A
[zoom]    $2E
[window] $48
[Y=]       $49

    Pour les autres valeurs, vous les trouverez dans tous les tutoriels comme asmguru... je n'ai pas envie de passer 3h à recopier ces valeurs qui servent rarement, c'est pas le but de ce tutoriel.

Afficher quelque chose à l'écran:

puts : Affiche le texte se trouvant à l'adresse pointée par hl (et terminé par un 0) , et dont les coordonnées 16 bits se trouvent dans les variables currow et curcol.  C'est la 'grosse' police qui est ici utilisée.

     ld    hl,3
    ld    (currow),hl   ;charge la valeur de la ligne de texte sur l'écran
    ld    hl,4
    ld    (curcol),hl   ;charge la valeur de la colone de texte.
    ld    hl,texte      ;charge l'adresse du texte dans hl
    bcall(_puts)        ;appelle la rom call puts
    ...
texte:
    .db"Hello world!",0 ; le 0 indique la fin de la chaine à afficher.


Note: Avec cette police, il y a 8 lignes et 16 colonnes.

vputs : Affiche le texte situé à hl et terminé par un 0, comme pour puts, sauf que c'est la petite police qui est utilisée. Les coordonnées du texte sont au pixel près, et sont stockées dans des variables 8 bits: currow et curcol.


     ld     a,20
    ld     (penrow),a   ;charge la valeur de la ligne de texte sur l'écran
    ld     a,30
    ld     (pencol),a   ;charge la valeur de la colone de texte.
    ld     hl,texte     ;charge l'adresse du texte dans hl
    bcall(_vputs)       ;appelle la rom call vputs

    ...
texte:
    .db"Hello world!",0



disphl: Tout ça c'est bien, mais des fois, on a besoin d'afficher une valeur..... disphl est là pour ça, et affiche la valeur de hl (en décimal) aux coordonnées (curcol,currow), c'est la grosse police qui est utilisée ici. Vous devriez comprendre parfaitement ceci:

ld     hl,3
ld     (currow),hl
ld     hl,4
ld     (curcol),hl
ld     hl,658
bcall(_disphl)


Le résultat est bien évidemment 658 affiché à l'écran....

Quelques Rom Calls graphiques

C'est bien d'afficher du texte, mais des fois, il faut afficher des trucs plus graphiques... Les 2 ROM Calls servent à tracer un point ou une ligne à l'écran...

ipoint: Celle ci affiche un point noir ou blanc à l'écranaux coordonnées (b,c)... Un autre paramètre sert ici: le fait que le pixel sera allumé ou éteint, ou change d'état, ce qui est indiqué par la valeur de d.

ld     b,5
ld     c,22
ld     d,1
bcall(_ipoint) ;affiche un point NOIR aux coordonnées (5,22)


Les valeurs principales de d sont 0 pour "pixel éteint (ou blanc)", 1 pour pixel alumé, 2 pour "pixel change d'état"

iline: Celle là trace une ligne à l'écran entre les points (b,c) et (d,e). Ici le paramètre n'est plus dans d mais dans h (0:ligne blanche,1:ligne noire,2:changement d'état des pixels de la ligne)...

NOTE: Dans un programme nécessitant de la VITESSE, évitez à tout prix les ipoint et iline qui sont très lent, travaillez plutôt directement avec le "graphbuffer" (prochain chapitre)...

Tout ça c'est bien beau, mais dans tout bon jeu, les graphismes ne se limitent pas à des pixels et des lignes, il y a aussi des images c'est donc le sujet du prochain chapitre...

Sprites, routines de ION

Sprites

Une sprite est une petite image, de 8 pixels de large, et de hauteur variable. Elle se compose d'une suite d'octets, dont chaque bit représente un pixel de la sprite. On la représente donc en binaire dans les sources pour la reconnaître plus facilement: J'ai mis en commentaire juste les 1 pour que vous voyiez la tête de la sprite...

sprite:

.db %01111100 ; 11111
.db %10000010 ;1     1
.db %10101010 ;1 1 1 1
.db %10000010 ;1     1
.db %10111010 ;1 111 1
.db %10000010 ;1     1
.db %00111100 ; 11111


J'en profite pour vous expliquer une petite chose: les .db que vous avez vu juste là, et au par avent, c'est en fait une syntaxe qui indique au compillateur que ce qui suit est une suite de données directement:

.db 2,2,198,7

Ceci permet de mettre à cet endroit les valeurs 2 2 198 et 7... Autre chose, les signes % ou $ que vous avez vu correspondent à la base employée pour exprimer la valeur: % correspond au binaire, $ à l'hexadécimal, pour le décimal, on ne met rien, le compillateur comprendra... on peut aussi voir 4ah pour de l'hexa, ou 00101101b pour du binaire, ça veut dire la même chose que les $ ou % au début...

Pour créer ses sprites, on peut utiliser paint pour faire une image bmp, puis la convertir avec un petit programme comme asmgfx qui est très bien...

Maitenant que nous avons notre sprite, il faut l'afficher... Pour cela, ion nous met à la disposition plusieurs routines:

ionputsprite: Cette routine permet de copier notre sprite dans le graphbuffer (mémoire graphique). En entrée, ionputsprite demande les coordonnées de la sprite dans respectivement a et l, l'adresse de cette sprite dans ix (un autre registre, peu utilisé car il ne permet pas grand chose, il est ici utilisé car h et l sont déjà pris...), et la hauteur de la sprite dans b:

ld     b,7          ;ici la sprite fait 7 pixels de haut
ld     a,15
ld     l,10
ld     ix,sprite
call   ionputsprite ;la sprite située à l'adresse sprite est copiée dans le graph buffer
                    ;aux coordonnées (15,10)


Seulement, toutes les sprites qu'on veut afficher ne font pas forcément 8 pixels de large au maximum! Pour copier des sprites plus grandes, on va utiliser une autre routine de ion: ionlargesprite. Les données que l'on met en entrée sont les mêmes que pour ionputsprite, mais on rajoute dans c le nombre d'octets qu'occupe la sprite dans la largeur (nombre de colonnes), autrement dit, on prend la largeur exacte de la sprite divisée par 8 et arrondi à l'entier supérieur...

ld     b,27           ;la sprite fait 27 pixels de haut
ld     a,10
ld     l,20
ld     c,2            ;elle fait 2 colonnes de large
ld     ix,sprite
call   ionlargesprite ; la sprite est copiée aux coordonnées (10,20)


sprite:
.db %00000011,%10000000
.db %00000101,%11000000
.db %00000111,%11000000
.db %00101011,%01111000
.db %00111101,%11111000
.db %00000101,%11111000
.db %00011110,%11111110
.db %00101001,%11111010
.db %01111011,%01111100
.db %01111111,%11111000
.db %01001111,%11101110
.db %00011000,%01111000
.db %00111111,%11000000
.db %00000001,%00000000
.db %00000001,%10000000
.db %00000001,%00000000
.db %00000001,%01000000
.db %00000001,%00000000
.db %00000001,%00000000
.db %00000001,%01000000
.db %00001011,%01101000
.db %00000111,%11110000
.db %00000111,%11110000
.db %00001000,%00001000
.db %00001000,%00001000
.db %00000111,%11110000


NOTE: Il faut à tout prix éviter de faire sortir les sprites de l'écran! Si la sprite dépasse à gauche ou à droite, c'est pas grave, elle ressortira de l'autre coté, mais si elle dépasse en haut ou en bas, c'est pas bon du tout! Il risque d'y avoir un ram cleared rapidement....

Ionfastcopy: On sait donc copier une sprite dans le graph buffer, seulement, ce n'est pas le graph-buffer qu'affiche l'écran directement, il faut appeller une autre routine pour que le contenu du graph buffer soit copié à l'écran: ionfastcopy. On fera juste call ionfastcopy pour cela... Cette routine demande pas mal de temps, il faut donc l'utiliser seulement quand c'est nécessaire, dans un jeu, une seule fois par image...

Autres routines de Ion

Certaines autres routines de ion sont intéressantes aussi:

ionrandom: Celle là donne un nombre "pseudo aléatoire" compris entre 0 et la valeur de b. Ca peut être intéressant, sinon, il existe une autre manière d'avoir des nombres à peu près aléatoires, c'est tout simplement de mettre la valeur du registre r (encore un autre, oui...) dans a: En fait, r est incrémenté à chaque instruction, donc se valeur change tout le temps.... Pour avoir un nombre "aléatoire", on fera donc:

ld     b,42 ;si on le veut compris entre 0 et 42
call   ionrandom
; a est ici un nombre aléatoire entre 0 et 42

ou plus simplement:

ld     a,r ;a est aléatoire compris entre 0 et 255

iongetpixel: Cette routine permet d'afficher dans le graphbuffer un pixel aux coordonnées (a,e). C'est beaucoup plus rapide
que d'utiliser ipoint, mais ça nécessite de faire après un ionfastcopy....

Listes et Matrices

Voilà un petit chapitre qui vous permettra de créer des listes et des matrices...

Listes:

Il est très facile de créer des listes en asm. Une liste va se présenter sous cette forme:

liste:
.db 4,52,35,0,34,7,3...

Pour connaitre par exemple la 3e valeur, on va simplement additionner l'adresse ou se trouve le début de la liste et 3. Pour avoir la valeur à l'abcisse b, voilà ce qu'on fait:

    ld b,3        ;je choisis de prendre le nombre d'abscisse 3
lectureliste:

    ld hl,liste
    ld e,b
    ld d,0
    add hl,de
    ld a,(hl)    ;ici, a=0
    .....

liste:
    .db 4,52,35,0,34,7,3...


La valeur est ici contenue dans a. Explication: on met dans hl l'adresse de la liste, comme on ne peut pas additionner un registre 8bits et un 16bits, on charge dans e b, et dans d 0. On additionne hl et de. On a donc hl qui est l'adresse de la valeur recherchée. On récupère donc cette valeur dans a. Dans notre cas, l'abcisse est 3, la valeur de a à la fin est donc 0 (la première valeur a pour abcisse 0)

Matrices:

On peut facilement assimiler une matrice à plusieurs listes de même longueur à la suite:

matrice:
    .db 5,32,54,2,14,90,1
    .db 4,52,35,0,34,7,3
    .db 1,42,3,4,85,6,17
    .db 5,32,54,2,14,90,1
    .db 4,52,35,0,34,7,3
    .db 1,42,3,4,85,6,17
    .........


Le code qui suit va lire la valeur dans la matrice aux coordonnées (b,c). Le principe est exactement le même que pour lire une liste, avec l'ordonnée en plus....

       ld b,3    ;on prends les coordonnées (3,2)
    ld c,2    ;

lecturematrice:
    ld a,c   ;\
    add a,a  ; |
    add a,a  ; |a=c*7
    add a,a  ; |
    sub c    ;/
    add a,b
    ld e,a
    ld d,0
    add hl,de
    ld a,(hl)

    ;ici, a=4

    ......

matrice:
    .db 5,32,54,2,14,90,1
    .db 4,52,35,0,34,7,3
    .db 1,42,3,4,85,6,17
    .db 5,32,54,2,14,90,1
    .db 4,52,35,0,34,7,3
    .db 1,42,3,4,85,6,17
.........



On commence par multiplier c par 7, en effet, comme on lit sur la 'c-ieme' ligne, il faut passer 7*c octets pour arriver sur la ligne qu'on veut. La suite est identique à la lecture dans une ligne.... Si vous comprenez pas encore, réfléchissez 2 minutes, c'est pas bien compliqué...

Le Direct Input

J'aurai dû en parler déjà il y a quelques temps mais bon. Je vous avais dit que la rom call getkey était un peu nulle, en fait, une autre raison est sa lenteur, en fait getkey gère plein d'autres trucs dont nous n'avons pas besoin quand nous l'appellons comme le link, l'APD, ou encore le contraste... En plus, elle va scanner tout le clavier, alors que la plupart du temps, seulement quelques touches sont nécessaires. Enfin elle ne retourne qu'une seule valeur, donc une seule touche peut être pressée en même temps. C'est nul. Heureusement pour nous, il y a un autre moyen de scanner le clavier: On va communiquer directement avec lui, grace à deux nouvelles instructions: in et out qui envoient ou reçoivent des données venant des ports de la calculette (le clavier en est un). En réalité, le clavier est divisé en petits groupes de touches (8 touches par groupe au maximum). Voici une routine de direct input expliquée:

directinput:
     ld     a,$FF            ;avant tout, on reset le keyport...
     out    (1),a            ;...en envoyant au port 1 la valeur 255 (FF)
     ld     a,$FE            ;on sélectionne le groupe du clavier à scanner(ici les flèches)...
     out    (1),a            ;...en envoyant au port du clavier (1) la valeur FE
     in     a,(1)            ;On récupère la valeur du port, qui indique quelle touche est pressée
     cp     254              ;\
     jp     z,bas            ;|
     cp     253              ;|on regarde suivant la valeur de a quelle touche a 
     jp     z,droite         ;|été pressée...
     cp     251              ;|
     jp     z,gauche         ;|
     cp     247              ;|
     jp     z,haut           ;/
bas:
....
droite:
....
gauche:
....
haut:
....

Voici les 7 groupes de touches, et les valeurs affectées pour chaque touche:

Groupe                                          $FE

[Bas]

254

[Gauche]

253

[Droite]

251

[Haut]

247

Groupe                                         $FD

[Enter]

254

[+]

253

[-]

251

[x]

247

[/]

239

[^]

223

[Clear]

191

Groupe III                                   $FB

[Moins2]

254

[3]

253

[6]

251

[9]

247

[ )]

239

[Tan]

223

[Vars]

191

Groupe IV                                   $F7

[ . ] 254

[2]

253

[5] 251
[8] 247
[ ( ] 239
[Cos] 223
[Prgrm] 191
[Stat] 127
Groupe V                                     $EF
[0] 254
[1] 253
[4] 251
[7] 247
[ , ] 239
[Sin] 223
[Apps] ou [Matrix] sur la 83- 191
[X,T,O,n] 127
Groupe VI                                   $DF
[Sto->] 254
[Ln] 253
[Log] 251
[x²] 247
[x^-1] 239
[Math] 223
[Alpha] 191
Groupe VII                                 $BF
[Graph] 254
[Trace] 253
[Zoom] 251
[Window] 247
[Y=] 239
[2nd] 223
[Mode] 191
[Del] 127

    La routine que je vous est donné est bien, mais elle ne permet toujours que de vérifier au maximum qu'une seule touche par groupe! Du moins, une seule peut être pressée à la fois: la valeur retournée par le port peut pas être égale à 254 et 251 en même temps... Pour en vérifier plusieurs il suffit d'observer toutes ces valeurs en bianaire:

254=%11111110
253=%11111101
251=%11111011
247=%11110111
239=%11101111
223=%11011111
191=%10111111
127=%01111111

En réalité, quand on appuie sur une touche, un 0 se met à une position bien particulière de l'octet, si on veut tester les touches indépendamment des autres touches, il faut donc juste tester ou sont les 0. Voici notre routine de direct input modifiée. Elle utilise une nouvelle instruction, pour vérifier l'étant d'un bit dans un registre : bit. Cette instruction met en fait le bit testé dans le flag zero/non-zero

directinput:
     ld     a,$FF            ;avant tout, on reset le keyport...
     out    (1),a            ;...en envoyant au port 1 la valeur 255 (FF)
     ld     a,$FE            ;on sélectionne le groupe du clavier à scanner(ici les flèches)...
     out    (1),a            ;...en envoyant au port du clavier (1) la valeur FE
     in     a,(1)            ;On récupère la valeur du port, qui indique quelle touche est pressée
     bit    0,a              ;si le bit 0 de a est...
     call   z,bas            ;...zéro, [bas] a été pressé!
     bit    1,a
     call   z,droite         ;[->]
     bit    2,a
     call   z,gauche         ;[<-]
     bit    3,a
     call   z,haut           ;[haut]
;suite du programme...
haut:
     push   af               ;La valeur de a ne doit pas être perdue!
     ;instructions à faire si haut est pressé....
     pop    af
     ret

.....

Attention: Pour pouvoir effectuer toutes les combinaisons de touches (haut+gauche, bas+droite,...) il faut faire appel à des call pour appeller chaque fonction, mais on doit s'arranger pour ne pas détruire la valeur du registre a pendant cette fonction, car a contient les infos sur quelle touche a été pressée! Sauvez donc a dans les fonctions dans un autre registre ou dans la pile.

Il y a d'autres moyens de tester les touches séparément que en utilisant bit, mais celui ci marche bien et n'est pas trop compliqué à comprendre...

La compilation

Pour compiler un programme, il faut d'abord s'assurer que tous les fichiers nécessaires à la compillation (que l'on trouve ici) ET la/les sources  sont bien copiés dans le même répertoire...

Ensuite, la commande est simple : taper (dans l'invité de commande ou commande MS dos) "asm mon_prog". Le programme retourne un fichier mon_prog.83p et/ou un fichier mon_prog.8xp...

Quelques explications:

ASM.BAT

C'est un fichier texte qui regroupe toutes les commandes nécessaires à la compilation, pour avoir à n'en taper qu'une seule... On peut le modifier comme on veut voilà un exemple de fichier asm.bat: (c'est celui livré avec ion)

@echo off
echo ----- Assembling %1 for the TI-83 Plus...
echo #define TI83P >temp.z80               
insère la ligne #define... pour indiquer à tasm qu'on compille pour 83+
if exist %1.z80 type %1.z80 >>temp.z80
if exist %1.asm type %1.asm >>temp.z80
tasm -80 -i -b temp.z80 %1.bin             
compile le programme pour la 83+
if errorlevel 1 goto ERRORS
devpac83 %1                                
lance devpac (à remplacer par qpack %1 sous windows 2000
copy %1.83p %1.8xp >nul
echo ----- Assembling %1 for the TI-83...
echo #define TI83 >temp.z80
                   insère la ligne #define... pour indiquer à tasm qu'on compille pour 83 -
if exist %1.z80 type %1.z80 >>temp.z80
if exist %1.asm type %1.asm >>temp.z80
tasm -80 -i -b temp.z80 %1.bin             
compile le programme pour la 83 -
if errorlevel 1 goto ERRORS
devpac83 %1                                
lance devpac sous win2K, ça marche pas... je connais pas le fichier pour la 83- sous win2K...
echo ----- Success!
echo TI-83 version is %1.83p
echo TI-83 Plus version is %1.8xp
goto DONE
:ERRORS
echo ----- There were errors.              
si il y a eu des erreurs
:DONE
del temp.z80 >nul                          
supprime les fichiers temporaires...
del %1.bin >nul

NOTE:Dans la pratique, tous les utilisateurs de windows2000/XP auront un problème avec ce fichier, en effet, devpac83 (comme devpac8x pour la 83+) ne fonctionnent plus dessus. La solution est simple, il faut télécharger sur ticalc un programme nomme "qpack" et remplacer dans le fichier asm.bat 'devpac83' par 'qpack' (qpack doit être dans le même repertoire que tout le reste bien sûr). Cependant, qpack ne marche que pour la 83+, je ne sais pas si il existe de programme pour la 83-....

 
Devpac83/devpac8x/qpack...

En fait, ces programme servent à changer légerement la structure du fichier binaire que TASM a donné en sortie pour qu'ils soient bien valides sur la calculette... en sortie, ils donnent un fichier .83p ou .8xp qui est le programme à envoyer sur la calculette.

TASM:

C'est le compillateur proprement dit, il va assembler la source pour donner un fichier binaire, qu'il faudra modifier un peu pour les différentes plateformes (le Z80 n'est (était) pas présent que sur les ti's!). C'est pour ça qu'on exécute ensuite devpac83...

ion.inc

C'est le fichier d'include de tout programme pour ion. Il inclut des données telles que les adresses où se trouvent les rom calls les plus utilisés (pas tous), mais aussi les adresses 'progstart' (en fait, c'est à cette adresse que le programme est toujours executé) 

Erreurs de TASM:

    Quand on débute en asm -et même après-, on peut toujours avoir des erreurs en compilant un programme, il est donc important d'avoir une idée de la signification de ces erreurs.

Tout d'abord, sachez que les lignes indiquées par tasm ne correspondent généralement pas avec les lignes dans le programme, cela donne cependant un ordre d'idée de l'endroit ou se trouve l'erreur.

"Unrecognised instruction" Cette erreur signifie que vous avez fait une faute de frappe dans le nom de l'instruction, comme faire 'addd' au lieu de 'add'

"Unrecognised argument" Ici, ce n'est pas sur l'instruction mais sur l'argument que se trouve la faite 'sub a,b' au lieu de 'sub b'

"Label not found" Soit vous avez oublié de mettre un label sur une routine, ou il y a une faute de frappe au niveau du saut qui l'appelle, ou du label....

"Range of relative branch exceeded" Vous savez qu'un jr (ou un djnz) ne peut sauter qu'à une adresse se trouvant au max à 128 octets de "distance". Dans ce cas, il faudra remplacer le jr par un jp ou modifier la place du jr, ou optimiser le code pour réduire la taille :)

"Duplicate label" le label indiqué se trouve 2 fois dans le fichier, il faut renommer le second...

"Label value misaligned" Erreur bizarre qui se vire quand on corrige les "duplicate label"

"Maximum number of args exceeded" Quand on met des .db ou des .dw pour faire une liste, un tableau ou encore mettre du texte, il ne faut pas dépasser 32 octets, soit 32 valeurs pour des .db ou 16 pour des .dw. si ce problème se produit, faire comme ça:

.db 1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9

cette liste devient:

.db 1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9
.db 0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9

"Unused data in MS byte of argument" C'est une erreur un peu générique, elle peut dire tout et rien... généralement, elle est collée à une autre erreur (tasm indique les 2 à la suite et les deux erreurs sont à la même ligne dans la source) Quand on corrige l'erreur qui était collée à celle ci, on a corigé les 2...

"Unrecognised token" Une erreur très bizarre, plutôt rare... je la comprends pas trop en fait...