Fonctionnement de la pile matérielle du 6502 (4/8)

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 :

  1. 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
  2. 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.
  3. 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).
  4. 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.