PARAMÈTRES ET SOUS-ROUTINES
L’entrée et la sortie de données nécessaires au
fonctionnement d’une sous-routine, peut se dérouler de quatre façons :
- En passant par les registres (A, X et / ou Y). C’est la meilleure méthode et la plus performante lorsqu’il s’agit de données constituées d’un ou deux octets
- En passant par des variables en RAM. Cette méthode est parfaite pour des données qui doivent rester en RAM et qui doivent servir à d’autres routines. Elle est souvent utilisée de manière conjointe à la première méthode.
- Les besoins en entrées de la sous-routine peuvent être intégrés au programme lui-même, juste après l’instruction JSR (comme par exemple une chaîne alphanumérique à afficher). Dans ce cas, la sous-routine utilise l’adresse de retour stockée sur la pile en guise d’adresse de début des données. Elle peut aussi utiliser un octet qui détermine le nombre de données ou un délimiteur afin de savoir de combien d’octets avancer l’adresse de retour avant d’exécuter l’instruction RTS (et d’éviter que le processeur ne traite les données comme une instruction).
- En passant par la pile matériel.
- Pile en page 1 : le programme appelant empile la (ou les) données grâce à PHA, puis récupère le (ou les) résultats grâce à PLA, au retour de la sous-routine.
- Pile virtuelle en page 0 : Idem précédemment, avec une pile de données différentes de la pile matérielle, ce qui peut faciliter certaines opérations sur la pile et l’adressage indirect. Par exemple, la pile de données peut être utilisée dans un processus en plusieurs étapes pour calculer une adresse, adresse qui à son tour peut être utilisée pour un accès indirect à un octet dans un tableau pour faire un calcul ou une opération logique.
- Pile en RAM : Idem pile virtuelle en page 0, mais pouvant typiquement servir pour des nombres en virgule flottante autres que des adresses nécessitant les modes d’adressage disponibles en page 0.
Examinons un
exemple dans lequel il s’agit d’utiliser la pile matériel pour une donnée
d’entrée. Dans cet exemple, notre sous-routine transforme un demi-octet
hexadécimal en octet ASCII, avec un octet en entrée et un octet en sortie, le
tout stocké sur la pile matériel.
<do_stuff> ; Get the nybble into A. Allowable value is 0-F.
PHA ; Other code can be put between the PHA and the PLA, as long
JSR NYB2ASCII ; as it doesn't care if NYB2ASCII overwrites A and X. The
PLA ; subroutine's input and output are protected on the stack though.
<do_stuff> ; Do something with the ASCII output, like add it to a string, display it, etc..
La sous-routine NYB2ASCII pourrait ressembler à ceci.
NYB2ASCII:
TSX
LDA $103,X ; 103 reaches past the return address, to
the input parameter.
CMP #$0A
; Anything below $0A will end up in the $30’s.
CLC ; CLC before the BMI so we only
have to do it once.
BMI n2a1
; For 9 or less, skip the next instruction.
ADC #7 ; $0A becomes $11, $0B becomes $12,
etc.. C is still clear.
n2a1:
ADC #$30 ; Whether the 7 got added above or not,
this gives the ASCII.
STA $103,X
; Put it on the stack, overwriting the input value. Note
RTS ; that we read and overwrote the
byte just behind the
; return address, leaving the return address
undisturbed.
Voici un autre
exemple dans lequel deux nombres de 16 bits non signés en entrée sur la pile,
sont multipliés pour donner un nombre de 32 bits non signé, en sortie sur la
pile.
On empile tout
d’abord les deux nombres à transformer, chacun avec le poids fort en premier,
de sorte que le poids fort prenne l’adresse la plus haute comme dans le
fonctionnement habituel du 6502.
<do_stuff> ; Get high byte of first input,
PHA ; and push it.
<do_stuff> ; Get low
byte of first input,
PHA ; and push it.
<do_stuff> ; Get high byte of second input,
PHA ; and push it.
<do_stuff> ; Get low byte of second input,
PHA ; and push it.
JSR UM_STAR
; Now you can call the subroutine below that does the multiplying.
; If you pull the product off
the stack now, the byte order will be:
PLA ; 2nd-highest byte
<do_stuff>
PLA ; high byte
<do_stuff>
PLA ; low byte
<do_stuff>
PLA ; 2nd lowest byte
<do_stuff>
Selon les opérations que vous souhaitez réaliser ensuite,
vous pouvez très bien laisser le premier résultat sur la pile.
Si l’opération
après la multiplication des deux nombres est de calculer la racine carrée pour
en obtenir la raison géométrique, alors il convient de ne pas toucher à la pile
tant que cette seconde opération n’est pas effectuée (la raison géométrique
équivaut à une moyenne logarithmique. La raison géométrique de 1 et 100 est de
10, et non de 50,5).
Pour accéder aux
données stockées sur la pile sans les dépiler et dans l’ordre de votre
préférence, vous pouvez continuer d’utiliser X comme index de pile (ainsi que
décrit dans l’exemple UM_STAR ci-dessous). En cas de TSX, vous devrez ajuster l’index en
tenant compte du fait que l’adresse de retour n’est plus sur la pile, et comme
décrit ci-après, qu’un PLA a déplacé les résultats sur la pile de $101,X à $104,X.
Par contre, sans TSX l’index est toujours valide et pointe bien sur les bonnes
données.
UM_STAR: LDA #0 ; Unsigned, mixed-precision
(16-bit by 16-bit input, 32-bit output)
PHA ; multiply. Add a variable byte to the stack,
initializing it as 0.
TSX ; Now 101,X holds that new
variable, $102,X and $103,X hold the return address
LSR $107,X ;and $104,X to $107,X holds the
inputs and later the outputs.
ROR $106,X
FOR_Y
16, DOWN_TO, 0 ; Loop 16x. The DEY, BNE in NEXT_Y below will drop
through on 0.
IF_CARRY_SET
CLC
PHA ; Note that the PHA (and PLA below)
doesn’t affect the indexing.
LDA $101,X
ADC $104,X
STA $101,X
PLA
ADC $105,X
END_IF
ROR
ROR $101,X
ROR $107,X
ROR $106,X
NEXT_Y
STA $105,X
PLA ; Retrieve the variable
byte we added at the top, cleaning up the stack.
STA $104,X ; Again note that the PLA changed
S but not X, so the $104 is still $104.
RTS
Note : Dans de nombreux cas, il serait plus judicieux de nommer les
données présentes sur la pile, en utilisant la macro EQU, plutôt que de se
contenter de $101,X ou équivalent.
Supposons
maintenant que vous ayez besoin d’une sous-routine avec quatre octets en entrée
et six octets en sortie.
Afin de servir de
la pile matérielle pour deux données supplémentaires en sortie, il faut donc
résoudre le problème de l’adresse de retour qui ne sera pas en haut de la pile
lors du RTS.
La meilleure
méthode consiste à faire de la place sur la pile avant d’appeler la
sous-routine :
PHA ; Push two dummy bytes onto the
stack to hold the
PHA ; positions open for outputs of
the subroutine called below.
<do_stuff> ; Prepare the subroutine input bytes to
be passed.
JSR subroutine
; Without changing the stack pointer, the subroutine
; can now give you two more bytes of output than
input.
Autre cas
pratique, celui où vous auriez besoin de plus de données en entrées qu’en
sortie.
Dans ce cas, il suffit
de dépiler les données inutiles après être sorti de la sous-routine.
Ainsi, pour
restaurer la hauteur de pile, faites :
<do_stuff> ; Set up inputs, reserve output byte
places in stack, etc..
TSX ; Mark the current stack position.
<do_more_stuff>
JSR <subroutine>
<do_stuff> ; Handle outputs, etc..
TXS ; Restore stack to the marker
set earlier, possibly also
; to put certain outputs at the top.
Évidemment le
registre X ne devra pas être modifié par la sous-routine, ou alors il faudra le
sauvegarder à son tour sur la pile.
Comme décrit
précédemment, la pile matériel n’est pas la seule solution en tant que pile de
stockage. Une pile en page zéro (ZP) présente par exemple l’avantage de
disposer de modes d’adressages supplémentaires.
Utiliser une autre
pile que la pile matériel permet aussi de résoudre le problème suivant : admettons
que la pile matériel en page 1 serve pour passer des paramètres à une
sous-routine qui elle-même doit fournir des paramètres à autre sous-routine.
Lors de l’entrée dans la seconde sous-routine, l’adresse de retour se retrouve
donc désormais au sommet de la pile, tandis que les données d’entrée sont
décalées vers le bas, ce qui donnera à coup sûr des résultats erronés.
Mettons en œuvre
cet exemple avec la sous-routine GEOMEAN.
GEOMEAN: JSR UM_STAR ; Multiply two 16-bit unsigned inputs on the
stack. Get 32-bit product.
JSR
SQRT ; Take a 32-bit input on the stack and get a
16-bit output, leaving two
RTS ; dummy bytes on stack (problem:
GEOMEAN’s return addr is still on top!)
Supposons que vous commenciez par empiler $1234 sur la pile matériel
(poids fort en premier, sur le dessus de la pile), puis que vous empiliez $5678
avant d’appeler la sous-routine GEOMEAN.
Lorsque le
pointeur de programme attend la première instruction après PHA et TSX dans le
programme UM_STAR, UM_STAR part à la recherche du premier nombre à multiplier ($1234
dans notre exemple), censé se trouver aux adresses $106,X et $107,X, puis du
second nombre ($5678 dans l’exemple) qui se trouve aux adresses $104,X et
$105,X. L’adresse de retour se trouve quant à elle indexée par $102,X et
$103,X.
Il y a cependant
maintenant deux adresses de retour au sommet de la pile : celle qui a fait
appel à UM_STAR et celle qui a fait appel à GEOMEAN. UM_STAR va donc ignorer
$1234 et multiplier $5678 avec l’adresse de retour de GEOMEAN, entraînant ainsi
un résultat erroné et l’écrasement de l’adresse de retour de GEOMEAN. C’est le
plantage assuré !
Vous vous
apercevez également que le fait d’utiliser JMP au lieu de la paire
d’instruction JSR / RTS peut entraîner des problèmes d’incompatibilité liés à
une mauvaise gestion des données empilées sur la pile matériel.
La pile en page zéro permet de contourner ce genre de problème, tout
simplement parce que l’adresse de retour n’est pas empilée sur cette dernière.
En plus de
l’adressage de la page zéro qui est plus performant que celui de la pile
matériel, signalons aussi les bénéfices suivants :
- Inutile de replacer les paramètres d’entrée sur la pile sans
arrêt, avec pour résultat immédiat une exécution plus rapide des étages de la
pile ;
- Réduction des effets liés à la récursivité (ce sujet sera
abordé dans un prochain article) ;
- Possibilité de placer plusieurs type (et quantité) de
valeurs ;
- Code plus compact qui permet un meilleur fonctionnement car
plus besoin de devoir empiler/dépiler des paramètres lors de l’appel de
sous-routines.
Revenons
maintenant à notre sous-routine qui transforme un demi-octet hexadécimal en
octet ASCII et voyons comment réaliser cette transformation en utilisant une
pile ZP.
X
servira de pointeur de pile, initialisé en début de programme pour indexer de
la manière suivante $00,X.
Pour
empiler en page zéro, il faut donc faire un DEX puis stocker en $0,X.
Voici
ce que donne la routine modifiée pour la pile ZP :
<do_stuff> ; Get the nybble into A. Allowable value is 0-F.
DEX
STA 0,X
; Other code can be put between the STA and the LDA, and
JSR NYB2ASCII
; NYB2ASCII’s input and output are protected on the stack.
LDA 0,X
; X must be preserved as the data stack pointer of course.
INX
<do_stuff> ; Do something with the ASCII output,
like add it to a string, display it, etc..
Note : Si vous avez besoin
du registre X dans le reste de votre programme, il vous faudra le sauver sur la
pile matériel.
La sous-routine NYB2ASCII devient :
NYB2ASCII: ; (The initial TSX is no longer
necessary.)
LDA
0,X ; Get the input
parameter from the data stack.
CMP
#$0A ; Anything below $0A
will end up in the $30’s.
CLC ; CLC before the BMI so we only
have to do it once.
BMI
n2a1 ; For 9 or less, skip
the next instruction.
ADC
#7 ; $0A becomes $11, $0B
becomes $12, etc.. C is still clear.
n2a1:
ADC #$30 ; Whether the 7 got added above or not,
this gives the ASCII.
STA
0,X ; Put it on the data
stack, overwriting the input value. Note
RTS ; that the return address is not
on this stack to worry about.
Procédons selon la
même logique pour la sous-routine UM_STAR.
UM_STAR: DEX ; Add a stack byte to use
as a temporary variable.
LDA #0
STA 0,X ; 0,X addresses the temporary
variable. These are in ZP.
LSR 4,X
ROR 3,X
FOR_Y 16, DOWN_TO, 0
IF_CARRY_SET ; The 1st time thru the loop, A needs 0;
so don’t use STZ above.
CLC
PHA
LDA 0,X
ADC 1,X
STA 0,X
PLA
ADC 2,X
END_IF
ROR
ROR 0,X
ROR
4,X
ROR 3,X
NEXT_Y
STA 2,X
LDA 0,X
STA 1,X
INX ; Take back the stack byte
we used as a temporary variable.
RTS
;——————
Ici, un octet est empilé sur la pile ZP grâce à l’instruction
DEX (puis dépilé grâce à INX), de la même manière que pour PHA (puis PLA). La
pile ZP permet donc d’éviter d’avoir à sauver/restaurer l’index X.
Il est en outre possible de se passer des instructions
DEX/INX en adressant le nouvel octet grâce à $FF,X qui permet de rester en page
zéro et qui économise au passage 2 octets et 4 cycles d’horloge.
Assurez-vous cependant de n’avoir aucune sous-routine ou
interruption susceptible d’écraser cet octet, car X semble indiquer que cet
emplacement est libre.
La sous-routine GEOMEAN quant à elle, peut désormais s’écrire :
GEOMEAN: JSR UM_STAR
; Multiply two 16-bit unsigned inputs on the stack and get a 32-bit
product.
JMP
SQRT ; (JSR, RTS) Take a 32-bit input on the stack and get a
16-bit output.
;——————
Voici dans le détail les avantages à travailler avec des
piles séparées :
- Le nombre d’octets
entrés est indépendant du nombre d’octets sortis. En d’autres termes, nul
besoin de nettoyer les octets de sortie factices ni de placer des octets
factices sur la pile ;
- En disposant de
données séparées, les adresses de retour ne sont pas traitées comme des
données. Le paramétrage devient implicite ;
- Une sous-routine
peut faire appel à une autre sous-routine sans avoir à faire le ménage sur la pile
matériel (qui devient désormais la pile de retour) ;
- La pile matériel
est maintenant disponible pour d’autres usages tels que la gestion des limites
et des index de boucles (y compris des boucles imbriquées) ;
- L’ajout d’une
nouvelle pile permet d’augmenter l’espace de stockage et d’éviter un crash sur
la pile matériel ;
- Grâce aux modes
d’adressages du 6502 en page zéro (ZP,X), nous pouvons adresser la pile ZP très
facilement.
Voyons justement comment mettre à profit les modes
d’adressage ZP,X dans l’exemple de routine de déplacement suivante.
Nous appellerons cette routine CMOVE, pour “character move”.
Avant de faire tourner cette routine, nous plaçons six octets
sur la pile ZP, sous la forme de 3 nombres de 16 bits (poids fort sur l’adresse
la plus haute dans chaque cas) :
- Adresse de départ
- Adresse de
destination
- Taille de mémoire
à déplacer (pouvant aller jusqu’à quelques dizaines de milliers d’octets)
CMOVE: LDA
0,X ; “See-move”
Character (memory) move ( from to len —
)
ORA
1,X
BEQ POP3
; If remaining length is 0, branch to POP3 which is just 6 INX’s, then
RTS.
LDA (4,X)
; Get a byte and
STA (2,X)
; transfer it.
INC 4,X
BNE cm1$
INC 5,X
; Increment the source addr
cm1$: INC
2,X
BNE cm2$
INC 3,X
; and the destination addr,
cm2$: DEC
0,X ; decrement the count
left,
LDA 0,X
CMP #$FF
BNE CMOVE
DEC 1,X
; and go back up for
another loop. If we’re
done,
BRA CMOVE
; that fact will be caught in the first three lines (BRA is for 65c02
only and stands for Branch Always).
Quand faut-il
éviter l’emploi d’une pile pour l’usage de paramètres d’entrées / sorties dans
des sous-routines ?
Déplacer des données dans une pile requiert du temps
processeur.
Aussi, si vous devez manipuler une grande quantité de
données, comme une chaîne alphanumérique, un tableau ou même une matrice,
placez là ailleurs en mémoire et indiquez son adresse de stockage sur la pile.
RÉCURSIVITÉ
Une sous-routine peut présenter la particularité d’être
récursive, à savoir qu’elle peut s’appeler elle-même à de nombreuses reprises.
Afin d’éviter que ce genre de sous-routine ne reboucle
indéfiniment, entraînant une surcharge de la pile matériel, il faut prévoir une
condition qui permette de quitter la sous-routine au bout de x boucles
récursives.
Lorsque cette (ou ces) condition(s) est respectée, on quitte
alors la sous-routine par une série de RTS.
Chaque sous-routine nidifiée (imbriquée) peut disposer de ses
propres variables ayant pourtant des
noms semblables.
Prenons l’exemple de la fonction de Fibonacci qui est
utilisée en calculs financiers, lors de la génération de nombres pseudo
aléatoires, dans des algorithmes de tri et de recherche ou encore pour la
compression de fichiers audio.
fiboIO: SETL $103
; For clarity, give names to the locals.
fiboTemp: SETL $102 ; (Record of X will be at $101,X.)
FIBO: CMP
#2 ; Test the input.
BCC end
; If it’s 0 or 1, so is the output, so just end.
; This
prevents endless nesting, too.
Otherwise,
PHA ; create local variable fiboIO,
and store A there.
PHA ; Create another local variable
byte, fiboTemp.
; (Its initial value
doesn’t matter.)
PHX ; Push X too, to protect the
calling routines.
TSX ; Prepare X to use for
stack-relative addressing.
DEA ; Get the n-1 term
JSR FIBO
; and calculate the number for it (recursing here)
STA fiboTemp,X
; and store its result in local variable fiboTemp.
LDA fiboIO,X
DEA
DEA ; Now get the n-2 term
JSR FIBO
; and calculate the number for it (recursing here).
CLC
ADC fiboTemp,X
; Add those two together, and
STA fiboIO,X
; store the answer. Don’t forget
the “,X”!
; Note that
there is no looping.
PLX ; Restore X for the calling
routines.
PLA ; Pull fiboTemp off the stack and
discard it.
PLA ; Pull the answer (fiboIO) off
the stack into A.
end: RTS
Cette sous-routine tient sur 34 octets et n’utilise pas la
RAM pour le stockage, hormis bien sûr la pile matériel.
La donnée d’entrée doit rester inférieure à 14 car la donnée
de sortie est sur 8 bits et le nombre de Fibonacci pour 13 est 233.
Avec une réponse sur deux octets ou plus, nous pourrions
avoir une donnée d’entrée plus grande sans manquer de place sur la pile, même
si ce genre de sous-routine en consomme beaucoup.
La place disponible sur la pile est largement suffisante pour
des sous-routines simples, mais rappelons que les routines récursives ne sont
pas des routines normales.
À suivre.