J'avais une cartouche « Atari Writer » qui traînait sur le bureau depuis quelques temps, et je voulais faire un exercice rapide de modélisation avec Blender.
Cela donne cette image, assez simple, mais qui a été un bon exercice.
J'avais une cartouche « Atari Writer » qui traînait sur le bureau depuis quelques temps, et je voulais faire un exercice rapide de modélisation avec Blender.
Cela donne cette image, assez simple, mais qui a été un bon exercice.
Comment donc les nombres aléatoires sont-ils générés sur un VG5000µ. C'est ce que je vous propose de suivre aujourd'hui en décortiquant le code.
Afin de suivre, il est important de comprendre comment les nombres sont stockés sur VG5000µ, et je vous propose pour cela un petit détour par cet article.
Petit rappel avant de commencer : un générateur de nombres aléatoires est une procédure qui émet une suite de nombres sur un intervalle, cette suite tentant d'avoir des propriétés intéressantes qui donnent l'illusion de l'aléatoire. La suite est cependant parfaitement définie, même si pas toujours simple à suivre, et c'est ce que nous allons voir par la suite.
Tout commence très tôt pour le générateur de nombres aléatoires. Dès l’initialisation de la machine, une série de valeurs est copiée depuis la ROM vers les variables systèmes. Cela se passe en $1071
, juste après l'initialisation de l'affichage.
ld hl,initvalues
ld bc,$0065
ld de,ramlow
ldir
Il y a donc 101 valeurs ($65) copiées depuis initvalues
($1194
) vers ramlow
($4830). Parmi celles-ci, les suivantes sont copiés vers $4844
et nous intéressent aujourd'hui.
defb $00,$00,$00
defb $35,$4a,$ca,$99
defb $39,$1c,$76,$98
defb $22,$95,$b3,$98
defb $0a,$dd,$47,$98
defb $53,$d1,$99,$99
defb $0a,$1a,$9f,$98
defb $65,$bc,$cd,$98
defb $d6,$77,$3e,$98
defb $52,$c7,$4f,$80
Les trois premiers octets sont les trois index avec lesquels le générateur va jouer. Nous les appellerons les trois seeds. Suivent 8 nombres plutôt grands, positifs et négatifs (le premier vaut -26514538
). Et enfin vient le nombre d'origine, qui vaut 0.8116351366043091
, que vous pouvez retrouver sous sa forme arrondie en tapant PRINT RND(0)
dès l'allumage du VG5000µ.
Cette table n'est pas la seule qui va être utilisée par le générateur. Il en existe une autre, qui sera utilisée depuis la ROM, en $093d
, d'une longueur de 3.
defb $68,$b1,$46,$68
defb $99,$e9,$92,$69
defb $10,$d1,$75,$68
L'instruction RND
commence en $090d
par un petit préambule qui vérifie l'argument passé à la fonction. Cet argument est disponible dans FAC
, l'accumulateur flottant (voir précédemment)
inst_rnd: rst getsign
ld hl,rnd_seed_2
jp m,reseed
ld hl,rnd_gen
call hl_to_fac
ld hl,rnd_seed_2
ret z
Ce préambule teste en premier lieu le signe de l'argument. S'il est négatif, la routine branche vers reseed
, que nous verrons plus loin. C'est au passage un comportement qui n'est indiqué ni dans le manuel d'utilisation du VG5000µ, ni dans les « Clés pour VG5000 », qui donnent de fausses informations (je vous laisse regarder).
Dans le cas du branchement, HL
point vers la seed 2 (et je ne vois aucun intérêt à ce que cela ne soit pas fait après le branchement...)
Si l'argument est nul ou positif, alors la nombre pointé par HL
, qui est la variable système du dernier nombre généré ,est copié dans FAC
. Souvenez-vous, c'est à cette adresse qu'a été placé à l'initialisation le nombre 0.8116351
.
On refait pointer HL
vers la seed 2 puis, si l’argument était 0
, on sort de la routine immédiatement (le flag Z
éro a été conservé depuis rst getsign
).
Puisqu'on est à présent dans le cas où l’argument est positif, il convient de générer un nouveau nombre. Ce nouveau nombre est basé sur, d'une part, le nombre généré précédemment, et d'autre part, les trois index qui avaient été initialisés à zéro (voir ci-dessus).
add a,(hl)
and a,$07
ld b,$00
ld (hl),a
inc hl
add a,a
add a,a
ld c,a
add hl,bc
call hl_to_bcde
call fp_mul
La première section du code précédent récupère l'index seed 2 en l'incrémentant de 1. En effet, A
contient 1 depuis l'appel à getsign
. Le résultat est pris modulo 8 et remonté en RAM.
Au passage, B
est initialisé à 0
pour que BC
puisse servir d'index, et HL
va pointer un cran plus loin, sur le début de la table des coefficients initialisés au boot (la table des 8 valeurs).
La seconde section calcul le pointeur dans cette table en quadruplant A
, qui est l'index, en format BC
comme index à ajouter au pointeur de base HL
.
L'appel hl_to_bcde
copie le nombre pointé dans la table vers BCDE
, puis l'appel à fp_mul
effectue la multiplication avec le contenu de FAC
.
Résumé : cette première étape est donc une multiplication du précédent nombre généré par un autre nombre, fixe, pris dans une table dans 8 valeurs tour à tour.
Le tout premier appel à RND(1) va multiplier 16129081
($98 $76 $1c $39) avec
0.8116351366043091(
$80 $4f $c7 $52`).
Cela donne 13090929
($98 $47$c0 $71
). Cela peut se vérifier dans FAC
($49e6
). Attention, le nombre est octet par octet dans le sens inverse à celui que j'utilise ici.
ld a,(rnd_seed_1)
inc a
and a,$03
ld b,$00
cp a,$01
adc a,b
ld (rnd_seed_1),a
ld hl,rnd_add - 4
add a,a
add a,a
ld c,a
add hl,bc
call fp_add_hl
afterreseed: call fac_to_bcde
La seconde étape se divise elle aussi en deux sections.
Dans la première section, on récupère la seed 1, qui est incrémentée de 1 et modulo 4. Cependant, la valeur 0 est interdite. Par une comparaison avec 1 et un ajout à 0 (via B
) avec retenue, si l'index était à 0, alors il est poussé à 1.
C'est donc en fait un index modulo 3 que l'on obtient.
Et cet index forme un pointeur via HL
de manière similaire à l'étape précédente, dans la table de trois valeurs de la ROM
mentionné au début de l'article.
Cette valeur est alors ajoutée à FAC
. L'appel à fp_add_hl
se charge de l'étape intermédiaire de chargement de la valeur dans BCDE
. Puis le résultat est ramené dans BCDE
.
Le label est un branchement venant du reseed
que nous verrons plus loin.
Résumé : cette seconde étape est une addition du nombre obtenue à la première étape avec un des trois nombres pris dans la deuxième table, pris tour à tour.
Le tout premier appel additionne 13090929
($98 $47 $c0 $71
) avec 4.626181e-08
($68 $b1 $46 $68
). Ce second nombre est bien trop petit par rapport au premier. Cette addition ne change rien... dans ce cas-ci. Nous verrons plus tard à quoi cette addition peut servir.
ld a,e
ld e,c
xor a,$4f
ld c,a
Dans cette troisième étape, le générateur fait des mélanges. Les 8 bits de poids les plus faibles sont mis de côté et les 8 bits de poids fort sont placés dans les 8 bits de poids faible.
Les 8 bits mis de côté sont XOR
és avec b01001111
. Ce qui signifie que certains bits sont inversés. Puis le résultat est placé dans les 8 bits de poids fort.
Cette opération ne me semble pas avoir de sens arithmétique. Cela semble être juste un mélange. Peut-être pour amener de l'entropie dans les bits de poids faible... Peut-être.
Résumé : cet étape mélange les parties de la mantisse et change quelques bits.
Le nombre est à présent 12501063
($98 $3e $c0 $47
).
ld (hl),$80
dec hl
ld b,(hl)
ld (hl),$80
Cette étape profite que HL
pointe actuellement (depuis la récupération de FAC
dans BCDE
) sur l'octet après FAC
pour préparer le terrain pour plus tard. Cet octet contient le complément à 1 du bit de signe du nombre de FAC
. En mettant cet octet à $80
, on force la valeur à être positive.
HL
pointe ensuite sur l'octet précédent, l'exposant, et met celui-ci à $80
, c'est-à-dire $2^ 0$ (voir l'article sur les nombres, toujours).
Résumé : le nombre final sera un nombre positif et l'exposant est fixé à $2^0 = 1$.
Pas d’influence, pour le moment sur le nombre tenu dans BCDE
.
ld hl,rnd_seed_0
inc (hl)
ld a,(hl)
sub a,$ab
jr nz,rnd_cnt
ld (hl),a
inc c
dec d
inc e
C'est la dernière étape du calcul. Dans la première partie, la seed 0 est incrémentée et récupérée dans A
.
Si la soustraction par 171
($ab
) n'est pas nulle, on branche plus loin à l'étape finale. Sinon, le résultat (0
) est replacé dans la seed 0. C'est donc un compteur jusqu'à 171
qui, lorsqu'il atteint cette valeur, modifie légèrement la mantisse.
Le premier et le troisième octets de la mantisse sont incrémentés, celui du milieu décrémenté, sans se soucier de débordements éventuels.
Résumé : une fois tous les 171 tirages, la mantisse est modifiée légèrement.
Comme ici, c'est le premier tirage, il ne se passe rien.
rnd_cnt: call bcde_norm
ld hl,rnd_gen
jp cpy_faclsb_hl
La mantisse a été générée, l'exposant est là. Mais tout nombre dans FAC
en sorti de routine doit être normalisé.
Comme indiqué dans l'article précédent sur la représentation des nombres, cela signifie que la mantisse et l'exposant vont être modifiée afin d'obtenir une mantisse avec un premier bit à 1 implicite, lui-même remplacé par le bit de signe.
MAIS ! Il y a un twist ! La routine bcde_norm
n'attend pas en entrée un nombre BCDE
, mais une mantisse 32 bits CDEB
. L'exposant du nombre actuel va donc se retrouver... en partie la moins significative du nombre, afin de nourrir, en quelque sorte, la partie droite de la mantisse lors de l'éventuel décalage vers la gauche.
C'est peut-être un peu obscure : je donne un exemple dans le résumé.
En sortie de normalisation, le nombre est bien dans FAC
. Le contenu de FAC
est alors copié à l'emplacement du dernier nombre généré.
C'est terminé !
Résumé : mise en forme du nombre, à la fois dans FAC
comme résultat de la fonction, et de côté pour servir de base au prochain nombre généré (ou pour être retourné en case de RND(0)
).
Nous en étions à $98 $3e $c0 $47
. Mais la normalisation s'attend à une mantisse 32 bits, et c'est donc comme ça que va être perçue la mantisse : $3e $c0 $47 $98
.
La normalisation doit déplacer la mantisse à gauche jusqu'à ce que le bit de poids fort soit à 1
. Il va falloir deux étapes pour cela :
$3ec04798
à $7d808f30
, puis$7d808f30
à $fb011e60
Comme le bit de poids fort du dernier octet n'est pas à 1, il n'y a pas d'arrondi. Cet octet est abandonné, le bit de signe et l'exposant corrigé par le nombre d'étapes ($80 - 2 donne $7e).
Au final, nous avons obtenu : $7e $7b $01 $1e
, soit 0.245121
Si l'argument de RND()
est négatif, alors un branchement a lieu sur une routine de réinitialisation du générateur.
reseed: ld (hl),a
dec hl
ld (hl),a
dec hl
ld (hl),a
jr afterreseed
À l'arrivée dans cette partie, HL
pointe sur seed 2 et A
est égal à $FF
. Ce qui a pour résultat de mettre les trois octets à $FF
.
Le branchement ramène dans le générateur à la fin de la seconde étape, c'est-à-dire après la multiplication et l'addition. Ce qui est dans FAC
(l'argument négatif de RND()
) est ramené dans BCDE
et le reste des étapes est effectué.
C'est donc une nouvelle séquence qui démarre, dépendante de l'argument passé à RND()
.
Revenons sur la seconde étape, l'addition avec un nombre tout petit. Dans l'exemple que nous avons suivi pendant l'article, l'addition ne servait à rien, car la différence entre les exposants était trop grand et donc le nombre à addition non significatif.
La question à se poser est donc : dans quels cas ces nombres deviennent-ils significatifs ?
Le plus petit d'entre eux est : $68 $46 $b1 $68
qui a pour exposant $68
. C'est-à-dire $80
- 24.
Il faut donc un nombre strictement inférieur à $00 $00 $00 $80
(0.5
) pour que l'addition soit intéressante.
Sauf que... juste avant l'addition, la multiplication a été faite avec un nombre dont l'exposant était au minimum $98
. Puisque dans une multiplication, les exposants s'ajoutent, cela implique que seuls des nombres avec un exposants à '$68' initialement vont être assez petits après la multiplication et être modifiés par l'addition.
C'est quelque chose de facile à déclencher en modifiant à la main le dernier nombre généré en mémoire. Mais est-ce que cela se passe si on laisse le générateur se dérouler normalement ?
Un expérience simple avec un debuggeur en mettant un point d'arrêt dans le code d'addition et en faisant tourner le générateur montre que... non. Cela n'arrive pas. Ou alors assez rarement pour résister à l'expérience.
Il est temps de se poser la question des bornes maximales et minimales des nombres générés.
Mais vu la longueur de l'article, ce sera pour le prochain...
Suite à l'article précédent, j'ai mis sur le dépôt GitHub un petit utilitaire Python qui reproduit les conversions entre la valeur du nombre et son codage en 4 octets.
Parfois, voir du code est plus simple qu'un long discours.
Et parce qu'on ne sait jamais trop quel sera la vie future du dépôt, voici le code des deux principales fonctions de l'outil.
import math
def get_byte(number):
"""Takes the current number, and returns the next byte encoding it with the reminder of the number to encode. """
number *= 256
result = int(number)
return result, (number - result)
def encode(number):
"""Gets a number, returns it's encoded four bytes (memory layout, so exponent at the end)."""
# If the number is zero, the encoding is immediate.
# In fact, only the exponent has to be 0.
if number == 0:
return [0, 0, 0, 0]
# Gets the sign from the number for later encoding
sign = 0x80 if number < 0 else 0
# We encode only positive numbers
number = abs(number)
# Shift the number so that the first fractional part bit
# of the mantissa is 1 (0.1 binary is 0.5 decimal)
exp = 0
while number >= 0.5:
number /= 2
exp += 1
while number < 0.5:
number *= 2
exp -= 1
# Gets the three bytes encoding the mantissa
o1, number = get_byte(number)
o2, number = get_byte(number)
o3, number = get_byte(number)
# Clears the most significant bit
# and replace it by the sign bit
o1 &= 0x7F
o1 |= sign
# Encode exponent
exp += 128
# Returns an array (Z80 memory layout)
return [o3, o2, o1, exp]
def decode(encoded):
""" Takes four encoded bytes in the memory layout, and returns the decoded value. """
# Gets the exponent
exp = encoded[3]
# If it's 0, we're done. The value is 0.
if exp == 0:
return 0
# Extract value from the exponent
exp -= 128
# Extract the sign bit from MSB
sign = encoded[2] & 0x80
# Sets the most significant bit implied 1 in the mantissa
encoded[2] = encoded[2] | 0x80
# Reconstruct the mantissa
mantissa = encoded[2]
mantissa *= 256
mantissa += encoded[1]
mantissa *= 256
mantissa += encoded[0]
# Divide the number by the mantissa, corrected
# by the 24 bits we just shifted while reconstructing it
mantissa /= math.pow(2, 24 - exp)
# Apply the sign to the whole value
if sign:
mantissa = -mantissa
return mantissa
Lors d'une discussion sur le forum system-cfg à propos de la fonction RND
une question a été posée sur le format des nombres dans le VG5000µ. C'est une question qui revient et que je voulais documenter pour mémoire, me posant régulièrement la question et oubliant juste après...
Distinguons déjà deux choses : les nombres manipulés par le système, et les nombres manipulés par le BASIC. Les premiers sont de diverses formes en fonction des besoins, de type entier, signé ou pas, sur 8 ou 16 bits la plupart du temps. Il n'y a pas grand chose à dire sur eux.
Les seconds sont ceux manipulés par le BASIC, qui est un BASIC Microsoft sur VG5000µ, et ce qui sera valide dans cet article le sera pour d'autres machines avec BASIC Microsoft et un processeur Z80. Au moins dans les grandes lignes, mais pour ce que j'ai vu en comparant une paire d'entre eux, souvent jusque dans les détails. Ces BASIC ne différent que de petites modifications ici ou là, de l'ordre de l'optimisation.
Le BASIC lui-même traite plusieurs types de nombres. On a déjà vu la manière dont il traitant de manière particulière les numéros de lignes après un GOTO
ou un GOSUB
dans l'article sur l'arrangement des lignes.
Le format qui nous intéresse aujourd'hui est celui utilisé pour les calculs, ainsi que celui utilisé dans les variables numériques. Dans l'article sur les variables, j'étudiais comment les variables étaient créées, avec leur nom suivis de 4 octets. Mais que contiennent ces 4 octets ?
Tous les nombres traités par le BASIC VG5000µ pour les calculs et les variables sont dans un format a virgule flottante. C'était déjà le cas avec le BASIC créé initialement à Dartmouth. L'idée était que les utilisateurs n'avaient pas à se poser de question sur le format interne, et pouvaient utiliser les nombres naturellement.
Plus tard, pour des raisons de performance (vitesse et consommation mémoire), des versions de BASIC ont ajouté un typage sur ces nombres. Comme le suffit %
des variables numériques qui indiquent que la variable contient un nombre entier.
Sur le VG5000µ, pas de typage de nombre. Tout est au même format. Je nommerai ce format le format BCDE
par la suite. BCDE
car il est manipulé la plupart du temps à travers cette paire de registres (BC
et DE
). BCDE
désigne donc aussi l'emplacement du nombre traité dans certaines situations.
Pour les calculs, le BASIC utilise un accumulateur flottant, que je nommerai FAC
par la suite (Floating point ACumulator). L'essentiel du format est le même que lorsqu'il est au format BCDE
.
L'accumulateur FAC
est l'endroit où se situe « le nombre en cours ». À la sortie d'une fonction, le résultat s'y trouve et est donc disponible pour les calculs ou fonctions suivants.
Ainsi, dans une expression comme INT(4 * 0.2)
, le FAC
va d'abord contenir 4
, puis après la multiplication 0.8
, puis après INT()
, contiendra 0
. Des instructions comme PRINT
ou LET
(explicite ou implicite), iront chercher cette valeur pour la traiter.
Puisque BCDE
et FAC
sont étroitement liés, il existe des fonctions pour transférer le contenu de BCDE
vers FAC
(\$05d2
) et inversement (\$05dd
). Il est possible aussi de récupérer dans BCDE
un nombre pointé par HL
(\$05e0
), et de monter dans FAC
un nombre pointé par HL
(\$05cf
), en utilisant BCDE
au passage.
Voici en premier lieu un tableau auquel je vais me référer pour expliquer le format.
B | C | D | E |
---|---|---|---|
Exp. | S|MSB | Milieu | LSB |
$49e9 | $49e8 | $49e7 | $49e6 |
FAC
l'accumulateur flottant.Attention : les adresses indiquées partent de la plus haute à la place basse. Lorsque vous inspectez la mémoire octet par octet, que ce soit dans FAC
ou pour une valeur de variable, le nombre est donc dans l'autre sens EDCB
, avec l'exposant en dernière position.
La mantisse couvre donc 23 bits. Le 24ième bit, le plus significatif, est implicitement à 1. Dans le format codé, il est donc remplacé par un bit de signe. Un bit à 1 indique un nombre négatif, positif sinon.
La mantisse est lue comme 0.1xxxxxx xxxxxxxx xxxxxxxx
en binaire. Les x
étant pris dans les 23 bits formés par MSB, Milieu et LSB.
L'exposant est centré sur 128 (ou $80 en hexadécimal). Cela signifie que 128 est l'exposant nul. Pour 129 l'exposant est 1, 127, et pour l'exposant est -1.
La valeur 0 ($00) pour l'exposant est spéciale : elle représente le nombre 0. Peu importe les autres octets, la valeur est nulle. Dans certains codage de nombre flottants, cet exposant zéro est utilisé pour représenter des valeurs particulières qui viennent compléter celles accessibles depuis un exposant non nul. Ici, ce n'est pas le cas. Un exposant à 0 signifie le nombre 0.
La valeur du nombre représenté est égal à : $-1 . signe . 2^{exp.} . mantisse$.
Ceci mérite quelques exemples.
Exemple 1 : 0
, codé 00 xx xx xx
. Comme je l'ai écrit ci-dessus, peut importe la valeur des autres octets. Si l'exposant est à 0, c'est le nombre 0.
Exemple 2 : 1
, codé 81 00 00 00
. En effet, 1 est égal à $b0.1 . 2^1$ (je préfixe les nombres en binaire par b
, pour faire la différence avec les nombres en base 10 que je ne préfixe pas).
L'exposant est donc 1, codé en 128 + 1 = 129, \$81 en hexadécimal. La mantisse est 'b0.1', codé en 00 00 00
puisque le premier 1
est implicite dans le codage.
Example 3 : 2
, codé 82 00 00 00
. Sur le même principe, 2 est égal à $b0.1 . 2^2$. Donc exposant 2 donne \$82 et la mantisse est là aussi 0 puisque b0.1 est implicite.
Exemple 4 : -2
, codé 82 80 00 00
. Le code est similaire à celui de 2
, mais le bit de signe négatif est placé sur le 7ième bit de l'octet le plus significatif de la mantisse.
Exemple 5 : 0.1', code ``7d 4c cc cc
. Là, c'est un peu plus compliqué. 0.1
n'est pas une valeur qui tombe juste en binaire. Puisque la précision est limitée, il faut bien s'arrêter quelque part ; il faut bien comprendre que le nombre 'retenu' n'est pas vraiment 0.1
, juste un nombre approchant.
Le nombre s'écrit $2^{-3} . b0.110011001100110011001100$... ; 128 - 3 donne \$7d en hex. Une fois enlevé le premier bit de la mantisse, le reste s'écrit comme indiqué.
Il est possible d'écrire
LET A = 0.1
PRINT A
Et le nombre affiché sera bien 0.1
. Pourtant, ce n'est pas le nombre encodé, qui est plus quelque chose comme : 0.09999999403953552
Mais ce nombre est arrondi lors de l'affichage.
À noter aussi que chaque opération effectuée par le BASIC, qui en interne utilise 32 bits, se termine par un encodage du nombre, qui est arrondi aux 24 bits disponibles. Si les calculs ont besoins de plus de 24 bits significatifs, alors les résultats ne seront pas exacts. Il faut se méfier des nombres flottants.
Rappelez-vous qu'en lisant la mémoire octet par octet, les nombres codés vont apparaître dans l'autre sens. Ainsi, si vous assignez à une variable le nombre 1
et que vous regardez dans la section de variable la valeur suivant le nom, vous y verrez 00 00 00 81
.
En examinant la mémoire de FAC
et en traçant les opérations, vous pourrez constater que l'octet suivant (\$49ea
) bouge aussi. Il s'agit d'un octet utilisé pour stocker le signe des opérations. En effet, afin de faire des opérations sur les mantisses, il faut en extraire le bit de signe et y remettre le bit implicite. Un bit de signe est stocké à cet endroit (attention, il s'agit de son complément).
Pour se faire une idée de comment sont traités les nombres, je vous invite à suivre le processus d'addition.
Tout commence en $0705
par la récupération sur la pile du nombre qui vient d'être décodé depuis la ligne du BASIC. Dans FAC
est déjà positionnée l'opérande précédente de l'opération.
eval_add: pop bc
pop de
jp fp_bcde_add
La nouvelle opérande est maintenance dans BCDE
dans le format expliqué dans les paragraphes précédents. Il faut donc maintenant ajouter BCDE
et FAC
.
En $0310
, la routine commence par un test. Si l'exposant, qui est initialement dans B
, est égal à 0
, on peut s’arrêter là. Ajouter 0
à FAC
signifie ne pas le modifier. Le retour est immédiat :
fp_bcde_add: ld a,b
or a,a
ret z
Deuxième cas simple, si la valeur de l'exposant de FAC
est 0, le résultat est le nombre présent dans BCDE
, il suffit donc de le transférer dans FAC
et c'est terminé :
ld a,(fac_exp)
or a,a
jp z,bcde_to_fac
L'étape suivante est de s'assurer que le nombre dans FAC
a un exposant plus grand que celui de BCDE
. Si ça le cas, c'est parfait, sinon, on échange les deux nombres. L'opération utilise la pile temporairement. A
qui contient la différence entre les deux exposants, prend la valeur de son opposée, pour rester cohérente.
sub a,b
jr nc,no_swap
cpl
inc a
ex de,hl
call fac_to_stck
ex de,hl
call bcde_to_fac
pop bc
pop de
À ce niveau, on a :
FAC
le nombre dont l'exposant est le plus grand,BCDE
, le nombre dont l'exposant est le plus petit,A
, la différence entre ces deux exposants.Si le nombre d'exposant le plus petit est insignifiant par rapport au plus grand, on peut s'arrêter là, le résultat de l'addition sera le plus grand des grands nombres. Puisque les nombres sont codés sur 24 bits significatifs, si la différences entre les exposants est de 25 ou plus, on peut sortir de la fonction :
no_swap: cp a,$19
ret nc
À présent que les nombres sont en place et qu'il est intéressant de les additionner, il faut les préparer. Pour le moment, le bit de signe est toujours codé dans les deux nombres. Ils sont aussi potentiellement à des exposants différents. Deux raisons pour lesquelles on ne peut pas additionner les mantisses pour le moment.
La première opération est d'extraire les signes et de renvoyer une indication sur l'opération à faire. Plus d'explication sur cette indication juste après.
Puis d'aligner les deux mantisses sur l'exposant le plus grand.
push af
call ext_sign
ld h,a
pop af
call div_mant
Je n'entre pas dans la routine ext_sign
, mais voici ce qu'elle fait. Elle prend le bit 7 de l'octet le plus significatif de FAC
et le remplace par le 1 implicite de la mantisse codée. Le bit de signe est inversé et est mis de côté, dans l'octet suivant l'exposant de FAC
.
Puis la même opération est faite sur BCDE
: extraction de signe, remplacement par 1. Le signe n'est pas mis de côté, mais est utilisé pour renvoyer dans A
une indication : 0
si les signes étaient identiques, 1
s'ils étaient opposés.
Cette indication, qui est sauvée dans H
pour pouvoir récupérer la valeur de AF
sauvée sur la pile (A
contient la différence entre les exposants), servira bientôt.
L'opération div_mant
décale vers la droite la mantisse de BCDE
d'autant de positions qu'indiquées par A
. Attention : en sortie de cette fonction, la mantisse est disponible sur CDEB
.
En effet, l'exposant n'est plus nécessaire, puisqu'il est identique à celui de FAC
. On peut donc réutiliser B
pour récupérer les bits résultants du décalage à droite et éviter de perdre trop de précision. La mantisse est temporairement sur 32 bits (même si seuls 24 sont toujours significatifs, puisque issus du nombre initial).
Il reste donc à passer à l'addition... enfin presque.
Dans la partie suivante, on récupère dans A
l'indicateur donné précédemment par les signes. Si les signes étaient identiques, on passe à la suite. S'ils étaient différents, c'est une soustraction qui sera faite, en allant vers min_bcde
.
ld a,h
or a,a
ld hl,fac_lsb
jp p,min_bcde
Pourquoi cette différence de traitement en fonction des signes ?
Le cas de l'addition des mantisses est le suivant :
call add_bcde
jr nc,round
inc hl
inc (hl)
jp z,overflow
ld l,$01
call shft_right
jr round
Après l'appel à add_bcde
qui ajoute la mantisse CDE
avec celle de FAC
octet par octet, un test est fait sur la retenue. Si l'addition n'a pas générée de retenue, alors on va vers la routine d'arrondi de FAC
, qui terminera l'opération.
S'il y a eu une retenue, il faut augmenter l'exposant de 1, ce qui est fait en pointant HL
un cran plus loin que la mantisse et augmentant la valeur de l'exposant qui s'y trouve. Si cette incrémentation a ramené l'exposant à 0
, c'est qu'il était déjà à son maximum. Le nombre est trop grand, une erreur de dépassement de capacité est lancée.
Sinon, il faut corriger la mantisse avec un décalage vers la droite de 1 bit (paramètre indiqué par L
) puis brancher vers la routine d'arrondi.
Le cas de la soustraction des mantisses est le suivant :
min_bcde: xor a,a
sub a,b
ld b,a
ld a,(hl)
sbc a,e
ld e,a
inc hl
ld a,(hl)
sbc a,d
ld d,a
inc hl
ld a,(hl)
sbc a,c
ld c,a
call c,compl2
Toute la première partie, jusqu'au call
, est la soustraction de la mantisse de 'FAC' par la mantisse 'CDEB' octet par octet. Le résultat se trouve dans 'CDE'. L'exposant du nombre final est toujours celui de FAC
.
Si le résultat de la soustraction est positif, alors il ne se passe rien de plus. Si une retenue a eue lieu pendant la soustraction, alors on prend son opposée en appelant compl2
, afin de rendre la mantisse positive. Cette routine inverse aussi le bit de signe qui avait était extrait dans l'octet suivant FAC
.
La routine qui suit dans le code étant la normalisation et arrondi d'un nombre, pas besoin de brancher, c'est terminée.
Les opérations pour obtenir le signe du résultat final ne sont pas forcément évidente à suivre. Voici ce qu'il se passe :
FAC
a été extrait de la première opérande.FAC
a été extrait de la première opérande.compl2
n'a pas été appelé et le résultat est positif. Dans le cas contraire, compl2
a changé le bit de signe et le résultat est bien négatif, puisque la mantisse est positive.FAC
portait l'indication négatif, et le résultat de la soustraction l'aura, en fonction des deux cas, laissé comme tel, ou inversé.La routine qui suit, de normalisation, s'assure que le bit de poids fort est toujours 1 (si ce n'est pas possible, c'est que le nombre est 0) et ajuste l'exposant en conséquence, puis arrondi le nombre. L'arrondi peut générer un dépassement de capacité.
L'addition n'a pas besoin de normalisation. Le résultat de l'addition des mantisses assure toujours que le bit de poids fort est 1 (au moins l'un des deux nombres était normalisé avec un bit de poids fort à 1, et si les deux l'étaient, alors cela a provoqué l'augmentation de l'exposant et le bit de poids fort est toujours 1, la retenu de 1 + 1 en binaire).
Le résultat de l'addition est à présent dans FAC
, place à la suite des opérations...
Depuis quelques temps, je planche à mes heures perdues sur ma prochaine vidéo. Et le sujet est le langage de programmation Logo. Après avoir survolé le BASIC, cela me semblait une suite logique.
En me plongeant dans l'univers Logo, j'ai cherché à mieux connaître le robot qui y est associé : la tortue Jeulin. Et j'ai été très étonné de voir aussi peu de ressources dessus. Et pourtant, il semble qu'elle ne soit pas si rare chez les collectionneurs.
Grâce à l'aide de photographie envoyées sur le forum system-cfg par Fool-DupleX (merci à lui), j'ai tenté une modélisation. Ça m'a pris... un certain temps.
Le résultat n'est pas correct au milimètre, mais donne une relativement bonne idée.
« (précédent) Page 10 / 23 (suivant) »