Techniques de programmation
Cette page présente les techniques de programmation courantes en assembleur 6502, incluant l'arithmétique multi-précision, la multiplication/division logicielle, la manipulation de chaînes, les tables de saut, le code auto-modifiant, les optimisations et les conventions d'appel de sous-routines.
Arithmétique 16 bits et multi-précision
Le 6502 est un microprocesseur 8 bits. Toute arithmétique sur des valeurs de plus de 8 bits doit être effectuée manuellement, octet par octet, en utilisant le drapeau C (Carry) pour propager la retenue entre les octets.
Addition 16 bits
Pour additionner deux valeurs 16 bits, on additionne d'abord les octets de poids faible (low bytes) avec ADC, puis les octets de poids fort (high bytes) avec ADC. Le drapeau Carry propage automatiquement la retenue.
- ; Resultat := Operande1 + Operande2 (16 bits)
- ; Operande1 stocke en OP1_LO / OP1_HI
- ; Operande2 stocke en OP2_LO / OP2_HI
- ; Resultat stocke en RES_LO / RES_HI
-
- CLC ; effacer la retenue avant l'addition
- LDA OP1_LO ; charger l'octet bas de l'operande 1
- ADC OP2_LO ; additionner l'octet bas de l'operande 2
- STA RES_LO ; stocker l'octet bas du resultat
- LDA OP1_HI ; charger l'octet haut de l'operande 1
- ADC OP2_HI ; additionner l'octet haut + retenue
- STA RES_HI ; stocker l'octet haut du resultat
Si C=1 après la deuxième ADC, il y a un débordement (le résultat dépasse 16 bits).
Soustraction 16 bits
Si C=0 après la deuxième SBC, le résultat est négatif (emprunt non satisfait).
Incrémentation 16 bits
Le 6502 n'a pas d'instruction INC 16 bits. On peut utiliser l'addition avec 1 :
Cette technique est plus rapide (5-8 cycles) que l'addition 16 bits complète (18 cycles).
Décrémentation 16 bits
Comparaison 16 bits
Le 6502 n'a pas de CMP 16 bits. On compare d'abord les octets de poids fort, puis les octets de poids faible si les octets hauts sont égaux :
- ; Comparer OP1 (16 bits) avec OP2 (16 bits)
- LDA OP1_HI
- CMP OP2_HI
- BNE Done_Cmp ; si octets hauts differents, flags valides
- LDA OP1_LO
- CMP OP2_LO ; comparer les octets bas
- Done_Cmp:
- ; A ce point, les drapeaux C, Z, N sont corrects
- ; BCC = OP1 < OP2 (non signe)
- ; BCS = OP1 >= OP2 (non signe)
- ; BEQ = OP1 == OP2
Décalage 16 bits
Décalage à gauche (multiplication par 2) :
- ASL VAR_LO ; decaler l'octet bas a gauche
- ROL VAR_HI ; decaler l'octet haut avec la retenue
Décalage a droite (division par 2, non signe) :
- LSR VAR_HI ; decaler l'octet haut a droite
- ROR VAR_LO ; decaler l'octet bas avec la retenue
Décalage a droite arithmétique (division par 2, signe) :
Négation 16 bits (complément a deux)
Arithmétique multi-précision (24 bits, 32 bits, ...)
Le principe s'étend a n'importe quelle taille. Il suffit de chaîner les opérations ADC/SBC sur chaque octet supplémentaire.
Coût en cycles : environ 6 cycles par octet supplémentaire
(LDA + ADC + STA = 3+3+3 = 9 cycles si en page zéro).
Multiplication et division en logiciel
Le 6502 ne possède aucune instruction de multiplication ou de division. Ces opérations doivent être implémentées en logiciel, a l'aide de décalages et d'additions.
Multiplication 8x8 → 16 bits
Algorithme de multiplication par décalage-et-addition (shift and add). On examine chaque bit du multiplicateur ; quand un bit est a 1, on additionne le multiplicande au résultat :
- ; RESULT (16 bits) := MCAND * MPLIER (8 bits chacun)
- ; MCAND = multiplicande (page zero)
- ; MPLIER = multiplicateur (page zero)
- ; RESULT = resultat 16 bits (RES_LO, RES_HI, page zero)
-
- Multiply_8x8:
- LDA #$00
- STA RES_HI ; initialiser le resultat haut a 0
- LDX #$08 ; compteur de bits (8 bits)
- Mult_Loop:
- LSR MPLIER ; decaler le multiplicateur a droite
- ; le bit de poids faible va dans C
- BCC No_Add ; si C=0, ne pas additionner
- CLC
- ADC MCAND ; additionner le multiplicande (A = RES_LO)
- BCC No_Add
- INC RES_HI ; propager la retenue dans l'octet haut
- No_Add:
- ROR RES_HI ; decaler le resultat 16 bits a droite
- ROR A ; (RES_HI:A) >> 1
- DEX
- BNE Mult_Loop
- STA RES_LO ; stocker l'octet bas du resultat
- RTS
Complexité : toujours 8 itérations, environ 100-150 cycles.
Multiplication par une constante
Pour des constantes spécifiques, il est souvent plus rapide d'utiliser des décalages et des additions :
- ; A := A * 2
- ASL A ; 1 instruction, 2 cycles
-
- ; A := A * 4
- ASL A
- ASL A ; 2 instructions, 4 cycles
-
- ; A := A * 3 (= A + A*2)
- STA temp ; sauvegarder A
- ASL A ; A := A * 2
- CLC
- ADC temp ; A := A*2 + A = A*3
-
- ; A := A * 5 (= A + A*4)
- STA temp
- ASL A
- ASL A ; A := A * 4
- CLC
- ADC temp ; A := A*4 + A = A*5
-
- ; A := A * 10 (= (A*4 + A) * 2 = A*5*2)
- ASL A ; A := A * 2
- STA temp
- ASL A
- ASL A ; A := A * 8
- CLC
- ADC temp ; A := A*8 + A*2 = A*10
-
- ; X := X * 40 (utile pour les ecrans 40 colonnes)
- ; 40 = 8 * 5 = 8 * (4 + 1)
- TXA
- ASL A
- ASL A
- ASL A ; A := X * 8
- STA temp
- ASL A
- ASL A ; A := X * 32
- CLC
- ADC temp ; A := X*32 + X*8 = X*40
Division 16/8 → 8 bits quotient, 8 bits reste
- ; DIVIDEND (16 bits) / DIVISOR (8 bits)
- ; -> QUOTIENT (8 bits), REMAINDER (8 bits)
-
- Divide_16_8:
- LDA #$00 ; initialiser le reste
- LDX #$08 ; compteur de bits
- Div_Loop:
- ASL DIVIDEND_LO ; decaler le dividende 16 bits a gauche
- ROL DIVIDEND_HI
- ROL A ; le bit sortant entre dans le reste (A)
- CMP DIVISOR ; reste >= diviseur ?
- BCC Div_Skip ; non, passer
- SBC DIVISOR ; oui, soustraire le diviseur
- INC DIVIDEND_LO ; mettre le bit de quotient a 1
- Div_Skip:
- DEX
- BNE Div_Loop
- STA REMAINDER ; stocker le reste
- ; Le quotient est dans DIVIDEND_LO
- RTS
Division par une puissance de 2
Diviser par une puissance de 2 est un simple décalage a droite :
- ; A := A / 2
- LSR A ; 1 instruction, 2 cycles
-
- ; A := A / 4
- LSR A
- LSR A
-
- ; A := A / 16
- LSR A
- LSR A
- LSR A
- LSR A
Modulo par une puissance de 2
Le reste d'une division par une puissance de 2 est obtenu par un AND avec le masque (puissance - 1) :
Manipulation de chaînes de caractères
Le 6502 n'a pas d'instructions de chaînes dédiées (contrairement au 80386 avec REP MOVSB). Les chaînes sont manipulées via des boucles utilisant l'adressage indexe ou indirect indexe.
Deux conventions de chaînes sont courantes :
- Chaînes Pascal : le premier octet contient la longueur (0-255), suivie des caractères. Utilisée par le Turbo Pascal.
- Chaînes ASCIIZ : terminées par un octet nul ($00). Utilisée par le C et les appels système.
Copie de chaîne ASCIIZ
- ; Copier la chaine a l'adresse (SRC) vers (DST)
- ; SRC et DST sont des pointeurs 16 bits en page zero
-
- StrCopy:
- LDY #$00
- CopyLoop:
- LDA (SRC),Y ; charger un caractere
- STA (DST),Y ; stocker dans la destination
- BEQ CopyDone ; si c'est le zero final, termine
- INY
- BNE CopyLoop ; continuer (max 256 caracteres)
- CopyDone:
- RTS
Longueur de chaîne ASCIIZ
Comparaison de chaînes ASCIIZ
- ; Comparer (STR1) et (STR2)
- ; Resultat : Z=1 si egales, Z=0 si differentes
- StrCmp:
- LDY #$00
- CmpLoop:
- LDA (STR1),Y
- CMP (STR2),Y
- BNE CmpDiff ; caracteres differents
- CMP #$00
- BEQ CmpDone ; fin des deux chaines (egales)
- INY
- BNE CmpLoop
- CmpDiff:
- ; Z=0 (chaines differentes), C indique l'ordre
- CmpDone:
- RTS
Recherche d'un caractère dans une chaîne
- ; Chercher le caractere dans A dans la chaine a (STR)
- ; Si trouve : C=1, Y = position
- ; Si non trouve : C=0
- StrChr:
- STA temp ; sauvegarder le caractere cherche
- LDY #$00
- ChrLoop:
- LDA (STR),Y
- BEQ ChrNotFound ; fin de chaine, pas trouve
- CMP temp
- BEQ ChrFound ; trouve !
- INY
- BNE ChrLoop
- ChrNotFound:
- CLC ; C=0 : pas trouve
- RTS
- ChrFound:
- SEC ; C=1 : trouve, Y = position
- RTS
Concaténation de chaînes ASCIIZ
- ; Concatener (STR2) a la fin de (STR1)
- ; STR1 doit avoir assez d'espace
- StrCat:
- LDY #$00
- FindEnd:
- LDA (STR1),Y ; trouver la fin de STR1
- BEQ CatCopy
- INY
- BNE FindEnd
- CatCopy:
- LDX #$00
- CatLoop:
- LDA (STR2,X) ; lire depuis STR2
- STA (STR1),Y ; ecrire a la fin de STR1
- BEQ CatDone ; zero final copie
- INX
- INY
- BNE CatLoop
- CatDone:
- RTS
Conversion majuscules
- ; Convertir la chaine a (STR) en majuscules
- StrUpper:
- LDY #$00
- UpperLoop:
- LDA (STR),Y
- BEQ UpperDone
- CMP #'a'
- BCC UpperSkip ; < 'a', pas une minuscule
- CMP #'z'+1
- BCS UpperSkip ; > 'z', pas une minuscule
- SEC
- SBC #$20 ; convertir en majuscule (a-z -> A-Z)
- STA (STR),Y
- UpperSkip:
- INY
- BNE UpperLoop
- UpperDone:
- RTS
Tables de saut
Les tables de saut (jump tables) permettent d'implémenter des structures switch/case ou des dispatchers efficaces.
Table de saut simple avec JMP indirect
On peut utiliser un index pour calculer l'adresse dans une table et effectuer un saut indirect :
- ; A contient le numero de commande (0, 1, 2, ...)
- ASL A ; A := A * 2 (chaque adresse fait 2 octets)
- TAX ; transferer dans X pour l'indexation
- LDA JmpTable,X ; lire l'octet bas de l'adresse
- STA JmpAddr
- LDA JmpTable+1,X ; lire l'octet haut
- STA JmpAddr+1
- JMP (JmpAddr) ; saut indirect
-
- JmpTable:
- .WORD Cmd_0 ; adresse du descripteur pour la commande 0
- .WORD Cmd_1 ; adresse du descripteur pour la commande 1
- .WORD Cmd_2 ; adresse du descripteur pour la commande 2
- .WORD Cmd_3 ; adresse du descripteur pour la commande 3
Table de saut avec RTS (technique "RTS trick")
Une technique classique du 6502 consiste a empiler une adresse sur la pile puis utiliser RTS pour sauter. RTS dépile l'adresse et y ajoute 1, donc on entrepose (adresse - 1) dans la table.
- ; A contient le numero de commande (0, 1, 2, ...)
- ASL A ; A := A * 2
- TAX
- LDA JmpTableHi,X ; octet haut de (adresse - 1)
- PHA ; empiler
- LDA JmpTableLo,X ; octet bas de (adresse - 1)
- PHA ; empiler
- RTS ; sauter a l'adresse empilee + 1
-
- JmpTableLo:
- .BYTE <(Cmd_0-1), <(Cmd_1-1), <(Cmd_2-1), <(Cmd_3-1)
- JmpTableHi:
- .BYTE >(Cmd_0-1), >(Cmd_1-1), >(Cmd_2-1), >(Cmd_3-1)
Avantages de la technique RTS :
- Pas besoin de pointeur temporaire en page zéro
- Les tables Lo et Hi séparées permettent l'indexation directe
- Plus rapide que le JMP indirect (évite le pointeur temporaire)
Table de saut avec JMP ($xxFF) - Bug du 6502 NMOS
Attention : sur le 6502 NMOS, l'instruction JMP indirect a un bug quand l'adresse du vecteur est a la fin d'une page ($xxFF). Au lieu de lire l'octet haut a $xx00 de la page suivante, il le lit a $xx00 de la MEME page.
Exemple :
JMP ($02FF) lit l'octet bas a $02FF et l'octet haut a $0200 (au lieu de $0300)
Ce bug est corrige sur le 65C02. Sur le 6502 NMOS, éviter de placer des pointeurs a des adresses de fin de page.
Code auto-modifiant
Le code auto-modifiant (self-modifying code, SMC) est une technique ou le programme modifie ses propres instructions pendant l'exécution. Sur le 6502, c'est une technique courante et souvent nécessaire pour contourner les limitations du microprocesseur.
Pourquoi le code auto-modifiant ?
Le 6502 n'a que 3 registres 8 bits (A, X, Y) et des modes d'adressage limites. Le code auto-modifiant permet de :
- Simuler des modes d'adressage inexistants
- Creer des pointeurs 16 bits sans utiliser la page zéro
- Optimiser des boucles internes
- Implémenter des sous-routines paramétrables
Modification d'une adresse
La technique la plus courante consiste a modifier l'adresse d'une instruction LDA/STA/etc. :
Ici, Load+1 est l'octet bas de l'adresse dans l'instruction LDA absolue (opcode $AD suivi de l'adresse en little-endian).
Copie de bloc mémoire rapide
En utilisant le code auto-modifiant, on peut créer une copie de bloc sans pointeurs en page zéro :
- CopyBlock:
- LDX #$00
- CopyBLoop:
- ReadAddr:
- LDA $1000,X ; l'adresse sera modifiee
- WriteAddr:
- STA $2000,X ; l'adresse sera modifiee
- INX
- BNE CopyBLoop
- ; Pour copier plus de 256 octets, incrementer les octets
- ; hauts des adresses :
- INC ReadAddr+2 ; incrementer la page source
- INC WriteAddr+2 ; incrementer la page destination
- ; ... continuer selon le nombre de pages ...
- RTS
Précautions avec le code auto-modifiant
- Le code doit être en RAM (pas en ROM)
- Sur les microprocesseurs avec cache (65C816), il peut y avoir des problèmes de cohérence de cache
- Le code est plus difficile a lire et a deboguer
- Il est impossible de mettre le code en ROM/EPROM
- Certains systèmes d'exploitation modernes interdisent l'écriture dans le segment de code (W^X), mais les systèmes 8 bits n'ont pas cette restriction
Optimisations courantes
Utiliser la page zéro
Les accès en page zéro sont plus rapides (1 cycle de moins) et les instructions sont plus courtes (1 octet de moins pour l'adresse). Placer les variables fréquemment utilisées en page zéro.
Dérouler les boucles (loop unrolling)
Au lieu d'une boucle avec branchement, répéter le corps de la boucle pour éliminer le coût du branchement :
Le déroulement est efficace quand la taille du code n'est pas un facteur limitant.
Utiliser X et Y au lieu de la mémoire
Les registres X et Y sont rapides pour les compteurs :
Tester un bit sans charger la valeur
L'instruction BIT teste les bits 7 et 6 directement sans modifier A :
- BIT $D011 ; teste le registre du VIC-II
- BPL Wait ; branche si bit 7 = 0
- BVS Skip ; branche si bit 6 = 1
Utiliser TXA/TYA au lieu de LDA
Si la valeur est déjà dans X ou Y, un transfert est plus rapide qu'un chargement mémoire :
- TXA ; 2 cycles
- ; vs
- LDA variable ; 3-4 cycles
Remplacer CMP #$00 par des tests implicites
Beaucoup d'instructions mettent déjà a jour les drapeaux N et Z. Un CMP #$00 supplémentaire est inutile :
Les instructions qui mettent a jour N et Z :
- LDA, LDX, LDY, TAX, TAY, TXA, TYA, TSX, PLA, AND, ORA, EOR, ADC, SBC, INC, DEC, INX, INY, DEX, DEY, ASL, LSR, ROL, ROR
Utiliser DEC/BNE au lieu de CMP pour les boucles
Quand on compte vers zero, DEC suivi de BNE est plus compact que CMP + BNE :
Multiplier/diviser par des puissances de 2
Utiliser les décalages ASL/LSR au lieu de routines de multiplication/division pour les puissances de 2 :
Exploiter le wraparound de la pile
La pile du 6502 fait 256 octets ($0100-$01FF) et enveloppe automatiquement (wraparound). On peut utiliser la pile comme un tampon en anneau sans vérification de débordement pour certaines applications.
Eviter les branchements quand possible
Les branchements pris coûtent un cycle de plus (3 cycles au lieu de 2) et un cycle supplémentaire s'ils traversent une frontière de page. Organiser le code pour que le cas le plus fréquent ne branche pas :
- ; Si value est generalement non-zero :
- LDA value
- BEQ RareCase ; branchement rarement pris (2 cycles)
- ; ... code frequent ...
Utiliser le mode décimal avec précaution
Le mode décimal (SED/CLD) est utile pour l'affichage de pointages et de compteurs BCD, mais :
- Il ralentit les opérations ADC/SBC d'un cycle sur le 65C02
- Le comportement des drapeaux N et V est indéfini en mode décimal sur le 6502 NMOS (corrige sur le 65C02)
- Toujours faire CLD dans le code d'initialisation et dans les descripteurs d'interruption
Tableau récapitulatif des coûts en cycles
| Opération | Cycles | Notes |
|---|---|---|
| LDA immédiate | 2 | La plus rapide |
| LDA page zero | 3 | Economique |
| LDA absolu | 4 | Standard |
| LDA absolu,X / absolu,Y | 4-5 | +1 si traversée de page |
| LDA (indirect),Y | 5-6 | +1 si traversée de page |
| LDA (indirect,X) | 6 | Toujours 6 cycles |
| STA page zéro | 3 | |
| STA absolu | 4 | |
| ASL A | 2 | Rapide pour multiplier |
| INC page zéro | 5 | Plus lent que INC registre |
| INX / INY | 2 | Rapide |
| JSR | 6 | Appel de sous-routine |
| RTS | 6 | Retour |
| BEQ/BNE (pris) | 3-4 | +1 si traversée de page |
| BEQ/BNE (non pris) | 2 | Rapide |
| JMP absolu | 3 | Saut direct |
| JMP indirect | 5 | 6502 uniquement |
Sous-routines et conventions d'appel
Appel et retour de base
L'instruction JSR (Jump to Subroutine) empile l'adresse de retour moins 1 (PC-1, soit le dernier octet de l'instruction JSR) sur la pile et transfère le contrôle au sous-programme. RTS (Return from Subroutine) dépile cette adresse, l'incrémente de 1 et reprend l'exécution.
- JSR ma_routine ; appel : empile (PC-1), saute
- ; ... execution continue ici apres RTS ...
-
- ma_routine:
- ; ... corps du sous-programme ...
- RTS ; retour : depile PC, PC := PC + 1
Etat de la pile lors de l'appel :
| État | Description |
|---|---|
| Avant JSR | SP → [ancien contenu] |
| Après JSR | SP → [PCL (adresse retour - 1, octet bas)] [PCH (adresse retour - 1, octet haut)] [ancien contenu] |
Note : JSR empile PC-1, pas PC. C'est pourquoi RTS fait PC := PC+1 après le dépilage. C'est une différence importante avec le 68000 (BSR/JSR empilent le PC exact) et le 80386 (CALL empile EIP exact).
Passage de parametres par registre
La méthode la plus simple et la plus rapide. Les paramètres sont places dans A, X, Y avant l'appel. Le résultat est généralement retourne dans A (ou A/X pour 16 bits).
Conventions courantes pour les registres :
| Registre | Usage typique |
|---|---|
| A | Paramètre principal / valeur de retour |
| X | Paramètre secondaire / index |
| Y | Paramètre tertiaire / index |
| C (Carry) | Indicateur d'erreur (C=1 souvent = erreur) ou retour booléen (C=1 = vrai) |
| A:X | Valeur de retour 16 bits (A=haut, X=bas) ou (X=haut, A=bas) selon la convention |
Passage de paramètres par la pile
Pour les sous-routines avec plus de 3 paramètres, ou pour la compatibilité avec du code généré par un compilateur, on peut passer les paramètres par la pile :
État de la pile dans le sous-programme :
- SP+5 → [arg3]
- SP+4 → [arg2]
- SP+3 → [arg1]
- SP+2 → [PCH (adresse retour)]
- SP+1 → [PCL (adresse retour)]
- SP → [sommet de pile]
Accès aux paramètres dans le sous-programme (via TSX) :
- ma_fonction:
- TSX ; X := SP
- LDA $0103,X ; arg1 (SP+3)
- ; ... utiliser les parametres ...
- RTS
Attention : les déplacements sont calcules par rapport a $0100+SP, car la pile se trouve dans la page $01.
Passage de paramètres inline (apres le JSR)
Une technique élégante du 6502 : les paramètres sont placés immédiatement après l'instruction JSR dans le code. Le sous-programme lit les paramètres en modifiant l'adresse de retour sur la pile.
- JSR PrintString
- .BYTE "Hello, World!", $00 ; chaine inline
-
- ; L'execution continue ici apres le RTS
-
- PrintString:
- ; L'adresse de retour sur la pile pointe sur
- ; le dernier octet du JSR, donc (adresse retour) + 1
- ; pointe sur le premier caractere de la chaine.
- PLA ; depiler PCL
- STA ptr_lo
- PLA ; depiler PCH
- STA ptr_hi
- ; ptr pointe maintenant sur (adresse retour - 1)
- ; Il faut incrementer de 1 pour pointer sur les donnees
- INC ptr_lo
- BNE NoCarry
- INC ptr_hi
- NoCarry:
- LDY #$00
- PrintLoop:
- LDA (ptr_lo),Y
- BEQ PrintDone ; zero final
- JSR OutputChar ; afficher le caractere
- INY
- BNE PrintLoop
- PrintDone:
- ; Ajuster l'adresse de retour pour sauter les donnees
- ; ptr + Y pointe sur le zero final
- ; RTS attend (adresse - 1), donc on empile ptr + Y
- CLC
- TYA
- ADC ptr_lo
- TAX ; X = (ptr + Y) bas
- LDA #$00
- ADC ptr_hi ; A = (ptr + Y) haut
- PHA ; empiler PCH ajuste
- TXA
- PHA ; empiler PCL ajuste
- RTS ; sauter apres les donnees inline
Sauvegarde et restauration des registres
Le 6502 n'a pas d'instruction MOVEM comme le 68000 ou PUSHA comme le 80386. La sauvegarde/restauration est manuelle :
- ma_routine:
- PHA ; sauvegarder A
- TXA
- PHA ; sauvegarder X
- TYA
- PHA ; sauvegarder Y
-
- ; ... corps du sous-programme ...
-
- PLA
- TAY ; restaurer Y
- PLA
- TAX ; restaurer X
- PLA ; restaurer A
- RTS
Important : les registres doivent être dépilés dans l'ordre INVERSE de l'empilement (LIFO).
Coût de la sauvegarde/restauration complète :
|
PHA + TXA + PHA + TYA + PHA = 3+2+3+2+3 = 13 cycles PLA + TAY + PLA + TAX + PLA = 4+2+4+2+4 = 16 cycles Total = 29 cycles de surcharge |
Pour les routines très courtes, il est souvent préférable de ne sauvegarder que les registres effectivement modifies.
Conventions d'appel par système
Chaque système a ses propres conventions :
Commodore 64 (KERNAL) :
- Les routines du KERNAL utilisent A, X, Y pour les paramètres et les retours
- Le Carry indique souvent une erreur (C=1 = erreur)
- Exemple : CHROUT ($FFD2) - A = caractère a afficher
- Exemple : CHRIN ($FFCF) - retourne le caractère dans A
- Exemple : SETLFS ($FFBA) - A=numéro logique, X=périphérique, Y=adresse secondaire
Apple II (moniteur) :
- Les routines du moniteur sont a $F800-$FFFF
- COUT ($FDED) : afficher le caractère dans A
- RDKEY ($FD0C) : lire une touche, retour dans A
NES :
- Pas de routines système en ROM
- Chaque jeu définit ses propres conventions
- Convention commune : paramètres en page zéro
Récursivité sur le 6502
La récursivité est possible mais limitée par la taille de la pile (256 octets). Chaque appel JSR consomme 2 octets (adresse de retour) plus les registres sauvegardes.
- ; Factorielle(n) : n dans A, resultat dans A
- ; Limitation : n <= ~40 (debordement pile et 8 bits)
-
- Factorielle:
- CMP #$01
- BCS FactRecurse ; si A > 1, recurser
- LDA #$01 ; factorielle(0) = factorielle(1) = 1
- RTS
- FactRecurse:
- PHA ; sauvegarder n sur la pile
- SEC
- SBC #$01 ; A := n - 1
- JSR Factorielle ; appel recursif
- ; A contient factorielle(n-1)
- STA temp
- PLA ; recuperer n
- ; Multiplier A * temp (simplification pour petit n)
- TAX
- LDA #$00
- FactMult:
- CLC
- ADC temp ; A := A + factorielle(n-1)
- DEX
- BNE FactMult ; repeter n fois
- RTS
Profondeur de pile typique par appel :
- JSR : 2 octets
- PHA (registres) : 1-3 octets
- Total par niveau : 3-5 octets
- Profondeur maximale : 256 / 5 = ~50 niveaux
En pratique, la récursivité profonde est déconseillé sur le 6502. Préférer les solutions itératives quand c'est possible.
Comparaison des conventions d'appel
| Microprocesseur | Sauvegarde retour | Passage paramètres | Sauvegarde registres |
|---|---|---|---|
| MOS 6502 | Pile (2 oct) | A, X, Y | Manuel PHA/PLA |
| Zilog Z80 | Pile (2 oct) | Registres | PUSH/POP paires |
| Intel 8080 | Pile (2 octets) | Registres | PUSH/POP paires |
| Motorola 6800 | Pile (2 octets) | A, B | PSHA/PSHB |
| Motorola 6809 | Pile (2 octets) | Registres | PSHS/PULS multi |
| Motorola 68000 | Pile (4 octets) | Registres ou pile | MOVEM.L |
| Intel 80386 | Pile (4 octets) | Pile (conv. C) | PUSHA/POPA |